Skip to content

Fix default route component remounting - #8002

Merged
Sheraff merged 5 commits into
mainfrom
codex/fix-default-route-remounting
Aug 12, 2026
Merged

Fix default route component remounting#8002
Sheraff merged 5 commits into
mainfrom
codex/fix-default-route-remounting

Conversation

@Sheraff

@Sheraff Sheraff commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • keep active Solid route components mounted by default by falling back to stable route identity
  • key Vue child matches by route ID instead of route ID plus serialized params
  • cache Vue Link inputs reactively so persistent route components see new params without per-location proxy/ref work
  • add public-API regression tests covering both default persistence and explicit param-based remounting

Root cause

Solid used the changing match ID as its implicit component key when no remountDeps function was configured. Vue independently included serialized strict params in the child Match key. Both identities changed during param-only navigation and forced active route components to unmount and mount again, contrary to the documented default.

Removing Vue's implicit param key exposed a separate assumption in Link: it copied declared props during setup and therefore relied on its parent remounting to receive param-derived values. The first correctness fix kept those props live with proxyRefs({ ...toRefs(props), ...attrs }).

That proxy was also the source of the broad Vue CPU regressions. Link has several location-driven computed values, and every navigation caused each Link to repeatedly cross the proxy and unwrap refs. Link-heavy scenarios amplified that fixed per-Link cost, which is why otherwise unrelated Vue navigation simulations moved together.

The follow-up keeps correctness with a computed plain snapshot. The snapshot rebuilds only when Link inputs change; location-only updates reuse the cached object.

Impact

Param-only navigation now preserves component instances and local state by default in Solid and Vue, avoiding unnecessary lifecycle work and garbage collection. Applications can continue to opt into remounting with remountDeps.

The focused Vue links benchmark recovered from 31.04 ms with the proxy/ref implementation to 17.15 ms (±1.13%). A controlled run with the old non-reactive plain spread was 17.46 ms, so the cached reactive version is back at the prior CPU cost while retaining correct updates in reused components.

Validation

  • public-API Link and remount regression tests: 133 passed
  • Vue unit suite: 829 passed, 1 skipped, no type errors
  • Solid client suite: 851 passed, 1 skipped, no type errors
  • Solid server suite: 3 passed, no type errors
  • dedicated Vue and Solid type targets across supported TypeScript versions
  • dedicated Vue and Solid ESLint targets (0 errors)
  • CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:vue --outputStyle=stream --skipRemoteCache -- scenarios/links/vue/speed.bench.ts
  • Prettier and git diff --check

Summary by CodeRabbit

  • Bug Fixes

    • Route components now remain mounted when route parameters change by default, preserving component state.
    • Configured remount dependencies still trigger expected remount behavior, including changes between falsy values.
    • Links now respond correctly to changing reactive properties, including navigation, preloading, styling, generated attributes, and unsafe URL handling.
  • Tests

    • Added coverage for component preservation, intentional remounting, reactive links, and preload cancellation across Solid and Vue Router.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Solid and Vue router match components now preserve mounted components during route parameter changes unless remountDeps requires remounting. Vue link handling now reads reactive options during evaluation and event execution.

Changes

Router runtime updates

Layer / File(s) Summary
Match remount behavior
packages/solid-router/src/Match.tsx, packages/vue-router/src/Match.tsx, packages/solid-router/tests/remountDeps.test.tsx, packages/vue-router/tests/remountDeps.test.tsx, .changeset/warm-routes-rest.md
Match keys now use route identity by default. Tests cover parameter updates with and without configured remount dependencies, including changes between falsy values. The changeset records patch releases for both routers.
Reactive Vue link options
packages/vue-router/src/link.tsx, packages/vue-router/tests/link.test.tsx
Link properties, navigation, preload behavior, SSR output, event handlers, styling, external-link handling, and merged props now retrieve current options through a lazy getter. Tests cover reactive updates and cancellation of stale preloads.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-default-route-remounting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 6m 32s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 48s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-12 11:20:55 UTC

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

2 package(s) bumped directly, 6 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/solid-router 1.170.25 → 1.170.26 Changeset
@tanstack/vue-router 1.170.24 → 1.170.25 Changeset
@tanstack/solid-start 1.168.42 → 1.168.43 Dependent
@tanstack/solid-start-client 1.168.24 → 1.168.25 Dependent
@tanstack/solid-start-server 1.167.31 → 1.167.32 Dependent
@tanstack/vue-start 1.168.41 → 1.168.42 Dependent
@tanstack/vue-start-client 1.167.27 → 1.167.28 Dependent
@tanstack/vue-start-server 1.167.31 → 1.167.32 Dependent

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 10826cc10e88
  • Measured at: 2026-08-12T11:15:14.588Z
  • Baseline source: history:31355ae92c00
  • Dashboard: bundle-size history

The following scenarios have bundle-size changes compared with the baseline:

Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
solid-router.minimal 33.17 KiB +4 B (+0.01%) 33.05 KiB 96.27 KiB 29.87 KiB ▆▅██████▁▁▁▁
solid-router.full 37.99 KiB +7 B (+0.02%) 37.87 KiB 110.90 KiB 34.17 KiB ▆▆██████▁▁▁▂
vue-router.minimal 49.58 KiB +124 B (+0.24%) 49.46 KiB 138.42 KiB 44.75 KiB ▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full 55.19 KiB +161 B (+0.29%) 55.07 KiB 156.63 KiB 49.65 KiB ▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal 45.95 KiB +5 B (+0.01%) 45.83 KiB 137.33 KiB 40.84 KiB ▁▅██████▁▁▁▂
solid-start.deferred-hydration 49.03 KiB +6 B (+0.01%) 45.89 KiB 144.79 KiB 43.69 KiB ▁▅█████▇▂▂▂▃
solid-start.full 51.03 KiB +12 B (+0.02%) 50.90 KiB 152.72 KiB 45.24 KiB ▁▄██████▁▁▁▂
vue-start.minimal 65.63 KiB +163 B (+0.24%) 65.51 KiB 189.24 KiB 58.42 KiB ▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full 69.44 KiB +144 B (+0.20%) 69.31 KiB 201.55 KiB 61.64 KiB ▁▃▅▅▅▅▅▅▂▂▂█

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@pkg-pr-new

pkg-pr-new Bot commented Aug 8, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@8002

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@8002

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@8002

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@8002

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@8002

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@8002

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@8002

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@8002

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@8002

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@8002

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@8002

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@8002

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@8002

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@8002

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@8002

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@8002

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@8002

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@8002

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@8002

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@8002

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@8002

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@8002

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@8002

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@8002

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@8002

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@8002

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@8002

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@8002

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@8002

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@8002

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@8002

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@8002

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@8002

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@8002

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@8002

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@8002

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@8002

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@8002

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@8002

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@8002

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@8002

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@8002

commit: 5ec9a20

@codspeed-hq

codspeed-hq Bot commented Aug 8, 2026

Copy link
Copy Markdown

Merging this PR will regress 0 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 20 improved benchmarks
❌ 12 (👁 12) regressed benchmarks
✅ 148 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem client unique-location-churn (vue) 1,368 KB 492.2 KB ×2.8
Memory mem server error-paths not-found (solid) 882.2 KB 575.9 KB +53.2%
Memory mem client loader-data-retention (vue) 1,023.4 KB 677.6 KB +51.04%
Simulation client-nested-params navigation loop (vue) 515.8 ms 343.1 ms +50.34%
Memory mem client unique-location-churn (solid) 453.8 KB 334.4 KB +35.7%
Memory mem server request-churn (react) 874.7 KB 710.2 KB +23.16%
Memory mem client interrupted-navigations (solid) 348.6 KB 289.5 KB +20.41%
Memory mem client interrupted-navigations (vue) 549.2 KB 459.4 KB +19.54%
Simulation client-loaders navigation loop (vue) 184.4 ms 168.4 ms +9.51%
Simulation client-history navigation loop (solid) 165.1 ms 152.9 ms +8%
Simulation client-search-params navigation loop (solid) 279.7 ms 262.7 ms +6.5%
Memory mem server error-paths not-found (react) 387.4 KB 366.4 KB +5.73%
Simulation client-rewrites navigation loop (solid) 212.9 ms 202.2 ms +5.32%
Simulation client-control-flow navigation loop (vue) 131.7 ms 125.2 ms +5.25%
Memory mem server peak-large-page (vue) 1,033.7 KB 987.7 KB +4.66%
Simulation client-async-pipeline navigation loop (vue) 141.6 ms 135.4 ms +4.57%
Memory mem client loader-data-retention (solid) 163.2 KB 156.6 KB +4.22%
Memory mem server server-fn-churn (react) 393.7 KB 379.1 KB +3.83%
Simulation client-side navigation loop (solid) 256.3 ms 247.4 ms +3.63%
Memory mem server error-paths unmatched (react) 429.9 KB 415.8 KB +3.4%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing codex/fix-default-route-remounting (5ec9a20) with main (dc5a404)

Open in CodSpeed

@Sheraff
Sheraff marked this pull request as ready for review August 12, 2026 07:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/vue-router/src/link.tsx (1)

104-185: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make external-link selection and props reactive.

type.value is read only during setup, so changes to to cannot switch between internal and external behavior. External props also capture the initial to, target, class, and disabled values. Compute the branch and external props from a reactive source. Add tests for both transition directions and external URL updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/src/link.tsx` around lines 104 - 185, The link branch and
external props in the setup flow must remain reactive when link options change.
Update the logic around the computed type and externalProps so it derives the
current options inside reactive computation, allowing `to` to transition between
internal and external URLs in both directions and ensuring external `href`,
`target`, `class`, and `disabled` values update; add tests covering both
transitions and external URL changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/vue-router/src/link.tsx`:
- Around line 116-117: Update the stable event-handler wrappers in the link
setup to resolve user handlers from the current options snapshot at event time
rather than from the setup-time initialOptions/eventHandlers. Preserve the
stable wrapper identity while ensuring replaced or removed handlers are
reflected for every event.
- Around line 898-901: Update LinkImpl’s allProps/linkPropsSource flow to
explicitly refresh the cached fallthrough attributes from attrs whenever the
parent updates, ensuring dynamic class, aria-label, and native listener changes
are observed. Add a regression test that updates a fallthrough attribute and
verifies LinkImpl reflects the new value.

---

Outside diff comments:
In `@packages/vue-router/src/link.tsx`:
- Around line 104-185: The link branch and external props in the setup flow must
remain reactive when link options change. Update the logic around the computed
type and externalProps so it derives the current options inside reactive
computation, allowing `to` to transition between internal and external URLs in
both directions and ensuring external `href`, `target`, `class`, and `disabled`
values update; add tests covering both transitions and external URL changes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8cd4609-8c8a-4f20-933d-0e0660ba7aee

📥 Commits

Reviewing files that changed from the base of the PR and between 7e93431 and ac30476.

📒 Files selected for processing (5)
  • packages/solid-router/src/Match.tsx
  • packages/solid-router/tests/remountDeps.test.tsx
  • packages/vue-router/src/Match.tsx
  • packages/vue-router/src/link.tsx
  • packages/vue-router/tests/remountDeps.test.tsx

Comment thread packages/vue-router/src/link.tsx Outdated
Comment thread packages/vue-router/src/link.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
packages/vue-router/src/link.tsx (4)

565-581: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant ...staticEventHandlers spread.

Line 565 spreads staticEventHandlers. Lines 571-581 then assign each handler with staticEventHandlers?.x ?? fallback. When staticEventHandlers is defined, the explicit assignments repeat the spread. When it is undefined, the spread contributes nothing. Delete Line 565 to keep one source for each handler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/src/link.tsx` around lines 565 - 581, Remove the
redundant ...staticEventHandlers spread from the props object near the explicit
event-handler assignments. Keep the existing staticEventHandlers?.x ?? fallback
expressions for each handler so handler precedence and behavior remain
unchanged.

169-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the duplicated type.value === 'external' guards.

The outer check at Line 178 evaluates once during setup. The inner check at Line 180 re-evaluates when type changes. The pair encodes a real behavior: a link that starts external and later becomes internal must begin subscribing to the location store, because useStore was skipped at setup. The code does not state this intent, so a later reader can remove one guard and break the external-to-internal transition.

Add a short comment that records the intent.

♻️ Proposed clarification
+  // Links that start external skip the `useStore` subscription above. If `to`
+  // later becomes internal, this effect re-runs and starts tracking location.
   if (type.value === 'external') {
     Vue.watchEffect((onCleanup) => {
       if (type.value === 'external') {
         return
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/src/link.tsx` around lines 169 - 192, Add a short comment
around the duplicated external-type guards in the currentLocation/watchEffect
setup, explicitly documenting that links changing from external to internal must
start subscribing to router.stores.location because useStore was skipped
initially. Preserve both guards and their existing transition behavior.

238-289: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Declare pendingPreload before enqueuePreload.

enqueuePreload reads and writes pendingPreload at Lines 244, 251, and 254, but the let declaration appears at Line 283. The code works because no call occurs before the declaration. Moving the declaration above the function removes the temporal dead zone risk if a future change calls enqueuePreload earlier.

♻️ Proposed reordering
+  let pendingPreload: 'intent' | 'viewport' | undefined
+
   const enqueuePreload = (
     e?: MouseEvent | FocusEvent | IntersectionObserverEntry,
   ) => {
-  let pendingPreload: 'intent' | 'viewport' | undefined
-
   useIntersectionObserver(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/src/link.tsx` around lines 238 - 289, Move the
pendingPreload declaration above the enqueuePreload function so all reads and
writes in enqueuePreload reference an initialized binding, preserving its
existing union type and behavior.

291-301: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use Vue.watchEffect for render preloading.

Vue.effect runs synchronously when preload or next changes. This can call doPreload() during component update processing. Vue.watchEffect uses pre-flush scheduling by default and follows the component effect scope.

♻️ Proposed change
-  Vue.effect(() => {
+  Vue.watchEffect(() => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/src/link.tsx` around lines 291 - 301, Replace the
Vue.effect used for render preloading with Vue.watchEffect, preserving the
existing preload.value check, next.value.href comparison, renderFetchedHref
update, and doPreload invocation. Keep the effect within the component’s
existing effect scope so it uses watchEffect’s default pre-flush scheduling.
packages/vue-router/tests/link.test.tsx (3)

541-543: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Move the data-status assertion inside waitFor.

Line 542 waits for the history pathname. The DOM patch that adds data-status="active" happens in a later Vue flush. Line 543 can run before that patch and cause a flaky failure.

💚 Proposed change
     await fireEvent.click(link)
-    await waitFor(() => expect(window.location.pathname).toBe('/posts'))
-    expect(link).toHaveAttribute('data-status', 'active')
+    await waitFor(() => {
+      expect(window.location.pathname).toBe('/posts')
+      expect(link).toHaveAttribute('data-status', 'active')
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/tests/link.test.tsx` around lines 541 - 543, Update the
test’s waitFor callback around fireEvent.click(link) so it waits for both the
/posts pathname and the link’s data-status="active" attribute. Remove the
separate assertion after waitFor while preserving the existing navigation
expectation.

5565-5583: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a positive control to this cancellation test.

All three assertions are negative. If intent preloading stops working completely, this test still passes. Add a final case where no input changes during the delay and assert that preloadRouteSpy is called once.

💚 Proposed addition
     await fireEvent.mouseEnter(link)
     to.value = 'https://example.com'
     await Vue.nextTick()
     await vi.advanceTimersByTimeAsync(50)
     expect(preloadRouteSpy).not.toHaveBeenCalled()
+
+    to.value = '/about'
+    await Vue.nextTick()
+    await fireEvent.mouseEnter(link)
+    await vi.advanceTimersByTimeAsync(50)
+    expect(preloadRouteSpy).toHaveBeenCalledOnce()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/tests/link.test.tsx` around lines 5565 - 5583, Add a
positive-control case to the existing link preloading cancellation test after
the disabled, changed-target, and external-URL scenarios: restore a preloadable
internal target, trigger mouseenter without changing inputs during the delay,
advance the timers, and assert preloadRouteSpy is called once.

603-611: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Separate handler removal from the disabled change.

Lines 603-605 clear both handlers and set disabled to true in one step. The assertions at Lines 610-611 then pass even if handler removal is broken, because a disabled link suppresses the outcome anyway. Clear the handlers first with disabled still false, assert the counts, then set disabled.

💚 Proposed change
     clickHandler.value = undefined
     mouseEnterHandler.value = undefined
-    disabled.value = true
     await Vue.nextTick()
     await fireEvent.click(link)
     await fireEvent.mouseEnter(link)
 
     expect(secondClick).toHaveBeenCalledOnce()
     expect(secondMouseEnter).toHaveBeenCalledOnce()
+
+    disabled.value = true
+    await Vue.nextTick()
+    await fireEvent.click(link)
+    await fireEvent.mouseEnter(link)
+
+    expect(secondClick).toHaveBeenCalledOnce()
+    expect(secondMouseEnter).toHaveBeenCalledOnce()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/tests/link.test.tsx` around lines 603 - 611, Update the
test around the disabled-link scenario to clear clickHandler and
mouseEnterHandler while disabled remains false, await the update, and assert
neither handler is invoked. Then set disabled to true in a separate update and
retain the existing disabled-link assertions, ensuring handler removal and
disabled behavior are tested independently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/vue-router/src/link.tsx`:
- Around line 562-582: The external-link props assembled by getExternalLinkProps
must match combineResultProps for disabled links: when options.disabled is true,
omit href and emit role="link" with aria-disabled, rather than forwarding a raw
disabled attribute. Update packages/vue-router/src/link.tsx lines 562-582
accordingly; update packages/vue-router/tests/link.test.tsx lines 470-494 to
assert aria-disabled and role instead of disabled attributes.

In `@packages/vue-router/tests/link.test.tsx`:
- Line 470: Update the assertions in the link tests around the external-link and
false-disabled cases to stop expecting a native disabled attribute on <a>.
Assert the accessible contract instead: the element retains role="link" and
communicates disabled state through aria-disabled when applicable, while enabled
links do not expose disabled="false".

---

Nitpick comments:
In `@packages/vue-router/src/link.tsx`:
- Around line 565-581: Remove the redundant ...staticEventHandlers spread from
the props object near the explicit event-handler assignments. Keep the existing
staticEventHandlers?.x ?? fallback expressions for each handler so handler
precedence and behavior remain unchanged.
- Around line 169-192: Add a short comment around the duplicated external-type
guards in the currentLocation/watchEffect setup, explicitly documenting that
links changing from external to internal must start subscribing to
router.stores.location because useStore was skipped initially. Preserve both
guards and their existing transition behavior.
- Around line 238-289: Move the pendingPreload declaration above the
enqueuePreload function so all reads and writes in enqueuePreload reference an
initialized binding, preserving its existing union type and behavior.
- Around line 291-301: Replace the Vue.effect used for render preloading with
Vue.watchEffect, preserving the existing preload.value check, next.value.href
comparison, renderFetchedHref update, and doPreload invocation. Keep the effect
within the component’s existing effect scope so it uses watchEffect’s default
pre-flush scheduling.

In `@packages/vue-router/tests/link.test.tsx`:
- Around line 541-543: Update the test’s waitFor callback around
fireEvent.click(link) so it waits for both the /posts pathname and the link’s
data-status="active" attribute. Remove the separate assertion after waitFor
while preserving the existing navigation expectation.
- Around line 5565-5583: Add a positive-control case to the existing link
preloading cancellation test after the disabled, changed-target, and
external-URL scenarios: restore a preloadable internal target, trigger
mouseenter without changing inputs during the delay, advance the timers, and
assert preloadRouteSpy is called once.
- Around line 603-611: Update the test around the disabled-link scenario to
clear clickHandler and mouseEnterHandler while disabled remains false, await the
update, and assert neither handler is invoked. Then set disabled to true in a
separate update and retain the existing disabled-link assertions, ensuring
handler removal and disabled behavior are tested independently.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c266438a-d53a-4f5a-927d-9b438f5c92a6

📥 Commits

Reviewing files that changed from the base of the PR and between ac30476 and 1f70347.

📒 Files selected for processing (7)
  • .changeset/warm-routes-rest.md
  • packages/solid-router/src/Match.tsx
  • packages/solid-router/tests/remountDeps.test.tsx
  • packages/vue-router/src/Match.tsx
  • packages/vue-router/src/link.tsx
  • packages/vue-router/tests/link.test.tsx
  • packages/vue-router/tests/remountDeps.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/vue-router/tests/remountDeps.test.tsx
  • packages/solid-router/tests/remountDeps.test.tsx
  • packages/solid-router/src/Match.tsx

Comment thread packages/vue-router/src/link.tsx
Comment thread packages/vue-router/tests/link.test.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/vue-router/tests/link.test.tsx`:
- Around line 5599-5604: Update the preload assertion in the test around
preloadRouteSpy to verify the call argument includes { to: '/about' }, rather
than checking only that it was called once. Preserve the existing single-call
expectation while asserting the final preload target.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 41039de6-b289-483c-8020-640e8219dd09

📥 Commits

Reviewing files that changed from the base of the PR and between 1f70347 and 5ec9a20.

📒 Files selected for processing (2)
  • packages/vue-router/src/link.tsx
  • packages/vue-router/tests/link.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/vue-router/src/link.tsx

Comment on lines +5599 to +5604

to.value = '/about'
await Vue.nextTick()
await fireEvent.mouseEnter(link)
await vi.advanceTimersByTimeAsync(50)
expect(preloadRouteSpy).toHaveBeenCalledOnce()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(link\.test\.tsx|.*router.*|.*link.*\.(ts|tsx|js|jsx))$' | head -200

printf '%s\n' '--- test lines 5560-5625 ---'
sed -n '5560,5625p' packages/vue-router/tests/link.test.tsx

printf '%s\n' '--- preloadRouteSpy references ---'
rg -n -C 5 'preloadRouteSpy' packages/vue-router/tests packages/vue-router/src packages/vue-router 2>/dev/null | head -300

Repository: TanStack/router

Length of output: 35260


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- dynamic test setup ---'
sed -n '5541,5608p' packages/vue-router/tests/link.test.tsx

printf '%s\n' '--- preloadRoute definitions and calls ---'
rg -n -C 4 'preloadRoute\s*[=:]|\bpreloadRoute\s*\(' packages/router-core packages/vue-router packages/react-router packages/solid-router 2>/dev/null | head -400

printf '%s\n' '--- files declaring Router.preloadRoute ---'
rg -l 'preloadRoute' packages --glob '*.{ts,tsx}' | head -100

Repository: TanStack/router

Length of output: 36136


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Vue Link intent preload implementation ---'
rg -n -C 12 'preloadRoute|preloadDelay|intent' packages/vue-router/src/link.tsx | head -300

printf '%s\n' '--- Router preloadRoute implementation ---'
rg -n -C 15 'preloadRoute\s*\(' packages/router-core/src/router.ts | head -300

printf '%s\n' '--- Link tests with argument assertions ---'
rg -n -C 4 'preloadRouteSpy.*(CalledWith|mock\.calls)|mock\.calls.*preloadRoute|toHaveBeenLastCalledWith|toHaveBeenCalledWith' packages/vue-router/tests/link.test.tsx packages/react-router/tests/link.test.tsx packages/solid-router/tests/link.test.tsx | head -300

Repository: TanStack/router

Length of output: 27099


Assert the final preload target.

toHaveBeenCalledOnce() checks only the call count. Assert that preloadRouteSpy was called with { to: '/about' } so a stale preload cannot satisfy the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/tests/link.test.tsx` around lines 5599 - 5604, Update the
preload assertion in the test around preloadRouteSpy to verify the call argument
includes { to: '/about' }, rather than checking only that it was called once.
Preserve the existing single-call expectation while asserting the final preload
target.

@Sheraff
Sheraff merged commit 3848503 into main Aug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actions github-actions Bot mentioned this pull request Aug 12, 2026
brenelz added a commit that referenced this pull request Aug 13, 2026
The #8002 fix itself is merged; its changeset stays on main for the 1.x
release and would otherwise trigger a stray vue-router/solid-router bump
in the rc pre-release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant