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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@SheraffSheraff 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

coderabbitaiBot 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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: fixing default route component remounting in Solid and Vue.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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-cloudBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5ec9a20

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded48sView ↗

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

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/solid-router1.170.25 → 1.170.26Changeset
@tanstack/vue-router1.170.24 → 1.170.25Changeset
@tanstack/solid-start1.168.42 → 1.168.43Dependent
@tanstack/solid-start-client1.168.24 → 1.168.25Dependent
@tanstack/solid-start-server1.167.31 → 1.167.32Dependent
@tanstack/vue-start1.168.41 → 1.168.42Dependent
@tanstack/vue-start-client1.167.27 → 1.167.28Dependent
@tanstack/vue-start-server1.167.31 → 1.167.32Dependent

@github-actions

github-actionsBot 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:

ScenarioCurrent (gzip)Delta vs baselineInitial gzipRawBrotliTrend
solid-router.minimal33.17 KiB+4 B (+0.01%)33.05 KiB96.27 KiB29.87 KiB▆▅██████▁▁▁▁
solid-router.full37.99 KiB+7 B (+0.02%)37.87 KiB110.90 KiB34.17 KiB▆▆██████▁▁▁▂
vue-router.minimal49.58 KiB+124 B (+0.24%)49.46 KiB138.42 KiB44.75 KiB▃▃▅▅▅▅▅▄▁▁▁█
vue-router.full55.19 KiB+161 B (+0.29%)55.07 KiB156.63 KiB49.65 KiB▃▃▄▄▄▄▄▄▁▁▁█
solid-start.minimal45.95 KiB+5 B (+0.01%)45.83 KiB137.33 KiB40.84 KiB▁▅██████▁▁▁▂
solid-start.deferred-hydration49.03 KiB+6 B (+0.01%)45.89 KiB144.79 KiB43.69 KiB▁▅█████▇▂▂▂▃
solid-start.full51.03 KiB+12 B (+0.02%)50.90 KiB152.72 KiB45.24 KiB▁▄██████▁▁▁▂
vue-start.minimal65.63 KiB+163 B (+0.24%)65.51 KiB189.24 KiB58.42 KiB▂▃▄▄▄▄▄▄▁▁▁█
vue-start.full69.44 KiB+144 B (+0.20%)69.31 KiB201.55 KiB61.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-newBot 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-hqBot 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

ModeBenchmarkBASEHEADEfficiency
Memorymem client unique-location-churn (vue)1,368 KB492.2 KB×2.8
Memorymem server error-paths not-found (solid)882.2 KB575.9 KB+53.2%
Memorymem client loader-data-retention (vue)1,023.4 KB677.6 KB+51.04%
Simulationclient-nested-params navigation loop (vue)515.8 ms343.1 ms+50.34%
Memorymem client unique-location-churn (solid)453.8 KB334.4 KB+35.7%
Memorymem server request-churn (react)874.7 KB710.2 KB+23.16%
Memorymem client interrupted-navigations (solid)348.6 KB289.5 KB+20.41%
Memorymem client interrupted-navigations (vue)549.2 KB459.4 KB+19.54%
Simulationclient-loaders navigation loop (vue)184.4 ms168.4 ms+9.51%
Simulationclient-history navigation loop (solid)165.1 ms152.9 ms+8%
Simulationclient-search-params navigation loop (solid)279.7 ms262.7 ms+6.5%
Memorymem server error-paths not-found (react)387.4 KB366.4 KB+5.73%
Simulationclient-rewrites navigation loop (solid)212.9 ms202.2 ms+5.32%
Simulationclient-control-flow navigation loop (vue)131.7 ms125.2 ms+5.25%
Memorymem server peak-large-page (vue)1,033.7 KB987.7 KB+4.66%
Simulationclient-async-pipeline navigation loop (vue)141.6 ms135.4 ms+4.57%
Memorymem client loader-data-retention (solid)163.2 KB156.6 KB+4.22%
Memorymem server server-fn-churn (react)393.7 KB379.1 KB+3.83%
Simulationclient-side navigation loop (solid)256.3 ms247.4 ms+3.63%
Memorymem server error-paths unmatched (react)429.9 KB415.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

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx Outdated
Comment threadpackages/vue-router/src/link.tsx Outdated

@coderabbitaicoderabbitaiBot 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 threadpackages/vue-router/src/link.tsx
Comment threadpackages/vue-router/tests/link.test.tsx Outdated

@coderabbitaicoderabbitaiBot 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/bashset -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/bashset -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/bashset -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 mainAug 12, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/fix-default-route-remounting branch August 12, 2026 12:53
@github-actionsgithub-actionsBot 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 freeto 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

@Sheraff