Skip to content

fix(router-core): use routeId for lifecycle hooks to fix onStay when loaderDeps change#6769

Merged
Sheraff merged 8 commits intoTanStack:mainfrom
sleitor:fix/router-lifecycle-use-routeid
Feb 27, 2026
Merged

fix(router-core): use routeId for lifecycle hooks to fix onStay when loaderDeps change#6769
Sheraff merged 8 commits intoTanStack:mainfrom
sleitor:fix/router-lifecycle-use-routeid

Conversation

@sleitor
Copy link
Contributor

@sleitor sleitor commented Feb 26, 2026

Summary

Fixes #6765

When loaderDeps change (e.g. from search param updates), the match.id changes because it includes the loaderDepsHash. The lifecycle hooks (onLeave/onEnter/onStay) were computing exitingMatches, enteringMatches, and stayingMatches using match.id, which caused a loaderDeps change on the same route to be treated as "leave + re-enter" instead of "stay".

Root Cause

match.id is composed as routeId + interpolatedPath + loaderDepsHash. When loaderDeps change (e.g. due to a search param update), the hash changes and match.id changes, making the old and new matches appear as different entries.

The loader's cause parameter already handles this correctly by using routeId:

// router.ts — loader cause uses routeId ✅
const previousMatch = previousMatchesByRouteId.get(route.id)
const cause = previousMatch ? 'stay' : 'enter'

But the lifecycle hooks were using match.id:

// router.ts — lifecycle hooks used match.id ❌
exitingMatches = previousMatches.filter(
  (match) => !newMatches.some((d) => d.id === match.id),
)

Fix

Use match.routeId (consistent with the loader's cause param logic) when computing exitingMatches, enteringMatches, and stayingMatches.

Test

Added a test case in callbacks.test.ts that verifies onStay fires (not onLeave/onEnter) when navigating within the same route and only loaderDeps-affecting search params change.

Summary by CodeRabbit

  • Bug Fixes

    • Separated lifecycle identity from caching identity so enter/leave/stay callbacks behave correctly when only route params or loader-driven dependencies change; ensures onStay fires instead of unnecessary onLeave/onEnter.
  • Tests

    • Added and expanded tests and helpers covering loader-driven dependency changes, same-route caching behavior, stale/gc timing, and navigation event payloads/counts.

…loaderDeps

When loaderDeps change (e.g. from search param updates), the match.id
changes because it includes the loaderDepsHash. This caused the lifecycle
hooks (onLeave/onEnter/onStay) to compute exiting/entering/staying matches
using match.id, which treated a loaderDeps change on the same route as
'leave + re-enter' instead of 'stay'.

Fix: use match.routeId (consistent with loader's cause param logic) when
computing exitingMatches, enteringMatches, and stayingMatches so that only
an actual route change triggers onLeave/onEnter.

Fixes TanStack#6765
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Separate cache identity (match.id: route+path+loaderDeps) from lifecycle-hook identity (routeId only) so onEnter/onLeave/onStay are determined by route presence regardless of loaderDeps changes; tests added for loaderDeps/search-param scenarios.

Changes

Cohort / File(s) Summary
Router core
packages/router-core/src/router.ts
Add lifecycle-hook identity groups hookExitingMatches, hookEnteringMatches, hookStayingMatches (based on routeId) while retaining cache identity groups exitingMatches, enteringMatches, stayingMatches (based on id). Switch lifecycle hook dispatch to use routeId-based groups and add explanatory comments.
Lifecycle tests
packages/router-core/tests/callbacks.test.ts
Add test helpers (e.g., createFooRouter, setupWithPathParams) and tests ensuring onStay fires for loaderDeps/search-param changes, regression tests for same-route match caching (staleTime/gcTime), and additional onBeforeNavigate assertions.
Manifest / metadata
packages/router-core/manifest.json
Minor manifest/line-count adjustments to reflect code and test changes.

Sequence Diagram(s)

sequenceDiagram
    participant Router
    participant PrevMatches
    participant NewMatches
    participant Cache
    participant LifecycleDispatcher

    Router->>PrevMatches: load previous matches (ids, routeIds)
    Router->>NewMatches: compute new matches (ids, routeIds)
    Router->>Cache: compute id-based groups (exiting/entering/staying by id)
    Router->>LifecycleDispatcher: compute routeId-based groups (hookExiting/hookEntering/hookStaying)
    LifecycleDispatcher->>Router: dispatch onLeave/onEnter/onStay using routeId groups
    Cache->>Router: run caching/GC using id-based groups
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • Sheraff
  • schiller-manuel

Poem

🐰 I hopped through ids and routes with care,
I kept the cache safe and let hooks declare,
When search params shift, the staybells ring,
One route remains — the lifecycle sings,
Tiny hops, same path, steady spring.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: using routeId for lifecycle hooks to fix onStay when loaderDeps change.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from #6765: lifecycle hooks now use routeId instead of match.id, fixing onStay to fire when loaderDeps change while preserving cache identity.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of fixing lifecycle hook identity by using routeId; cache identity separation and comprehensive test coverage are within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/router-core/tests/callbacks.test.ts (1)

138-161: Consider asserting onStay payload semantics, not only counts.

Counts are good; adding cause: 'stay' assertions would make this regression test harder to accidentally bypass.

Suggested assertion hardening
       await router.navigate({ to: '/foo', search: { page: '2' } })
       expect(onEnter).toHaveBeenCalledTimes(1) // no new onEnter
       expect(onLeave).toHaveBeenCalledTimes(0) // no onLeave
       expect(onStay).toHaveBeenCalledTimes(1) // onStay fires
+      expect(onStay).toHaveBeenNthCalledWith(
+        1,
+        expect.objectContaining({
+          cause: 'stay',
+          search: expect.objectContaining({ page: '2' }),
+        }),
+      )

       // Update again — onStay fires again
       await router.navigate({ to: '/foo', search: { page: '3' } })
       expect(onEnter).toHaveBeenCalledTimes(1)
       expect(onLeave).toHaveBeenCalledTimes(0)
       expect(onStay).toHaveBeenCalledTimes(2)
+      expect(onStay).toHaveBeenNthCalledWith(
+        2,
+        expect.objectContaining({
+          cause: 'stay',
+          search: expect.objectContaining({ page: '3' }),
+        }),
+      )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/tests/callbacks.test.ts` around lines 138 - 161, The
test currently only checks invocation counts for onStay; strengthen it by
asserting the payload semantics include cause: 'stay' when loaderDeps change:
after calling setupWithLoaderDeps and performing router.navigate to the same
path with updated search (the second and third navigate calls), inspect the
arguments passed to the onStay mock (onStay.mock.calls) and add expectations
that the event object passed to onStay contains a property cause with value
'stay' (and optionally that other expected fields like the new search are
present) so the assertions validate both invocation and correct payload
semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/router-core/tests/callbacks.test.ts`:
- Around line 138-161: The test currently only checks invocation counts for
onStay; strengthen it by asserting the payload semantics include cause: 'stay'
when loaderDeps change: after calling setupWithLoaderDeps and performing
router.navigate to the same path with updated search (the second and third
navigate calls), inspect the arguments passed to the onStay mock
(onStay.mock.calls) and add expectations that the event object passed to onStay
contains a property cause with value 'stay' (and optionally that other expected
fields like the new search are present) so the assertions validate both
invocation and correct payload semantics.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4027b6 and a8f88f2.

📒 Files selected for processing (2)
  • packages/router-core/src/router.ts
  • packages/router-core/tests/callbacks.test.ts

@nx-cloud
Copy link

nx-cloud bot commented Feb 26, 2026

View your CI Pipeline Execution ↗ for commit e485707

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

☁️ Nx Cloud last updated this comment at 2026-02-27 21:05:04 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 26, 2026

More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 4b0f787

@Sheraff
Copy link
Contributor

Sheraff commented Feb 26, 2026

@schiller-manuel do you agree that when params change (e.g. nav from /a/123 to /a/456) we want to trigger the onStay callback for the /a/$id route? The PR makes sense to me but this is a slight semantic change. The docs aren't exactly super specific about this: they describe the stay/enter/leave concepts as something like "a route is matched after not being matched in the previous location".

To me this change makes sense, just asking for a second opinion.

@Sheraff
Copy link
Contributor

Sheraff commented Feb 26, 2026

@sleitor don't worry about the "Bundle Size" job failing, this is unrelated to your PR, i'm working on it.

@sleitor
Copy link
Contributor Author

sleitor commented Feb 26, 2026

Thanks for the heads up @Sheraff, appreciated!

Comment on lines 2418 to 2431
exitingMatches = previousMatches.filter(
(match) => !newMatches.some((d) => d.id === match.id),
(match) =>
!newMatches.some((d) => d.routeId === match.routeId),
)
enteringMatches = newMatches.filter(
(match) =>
!previousMatches.some((d) => d.id === match.id),
!previousMatches.some(
(d) => d.routeId === match.routeId,
),
)
stayingMatches = newMatches.filter((match) =>
previousMatches.some((d) => d.id === match.id),
previousMatches.some(
(d) => d.routeId === match.routeId,
),
Copy link
Contributor

Choose a reason for hiding this comment

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

This change might mess up the caching behavior: a cache entry should be route id + params + deps (what we have currently). But I may agree that as far as lifecycle events are concerned, it may not exactly correlate with the cache changes.

I had codex whip up a quick test showing the expected cache behavior #6772. This test works on main and should fail w/ your PR.

Now we need to decide the semantics of those lifecycle events. Are they tied to the cache entries' lifecycles? Or are they more tied to the route itself (i.e. routeId)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a great distinction to make. I think cache entries and lifecycle events serve different purposes and shouldn't necessarily share the same identity:

  • Cache entries are about data identity — route + params + deps makes sense here, because different params or deps mean different data
  • Lifecycle hooks are about route presence — onEnter/onStay/onLeave describe whether a route is appearing, persisting, or disappearing in the matched tree

From a user perspective, if I navigate from /posts/123 to /posts/456, I'd expect onStay for the /posts/$id route (the route is still there, just showing different data), not onLeave+onEnter. The cache correctly gets a new entry for 456, but the route itself didn't leave.

That said, I'm happy to defer to whatever semantics you and @schiller-manuel decide are correct here. If the conclusion is that lifecycle events should follow cache identity, I can revert and we can look for a different fix for #6765.

Copy link
Contributor

Choose a reason for hiding this comment

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

the semantics as you described them are correct. so we need to fix the caching regression that @Sheraff pointed out and then we can merge this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion — done in the latest commit. Merged setupWithLoaderDeps and setupWithLoaderDepsForCaching into a single createFooRouter helper that accepts all options (onEnter/onLeave/onStay, loader, staleTime). All 9 tests still pass.

…ook identity (routeId)

Cache entries use match.id (routeId + params + loaderDeps) so navigating between
different params/deps correctly caches the previous match. Lifecycle hooks (onEnter/
onStay/onLeave) use routeId to track route *presence* in the matched tree — so
navigating /posts/123 → /posts/456 fires onStay for /posts/$id, not onLeave+onEnter.

Addresses caching regression spotted by @Sheraff in PR TanStack#6772 test.
@sleitor
Copy link
Contributor Author

sleitor commented Feb 27, 2026

Fixed in commit c8be9de — the caching regression is resolved by separating the two concerns:

  • Cache identity (//) still uses match.id (routeId + params + loaderDeps), so navigating /foo?page=1/foo?page=2 correctly moves the page=1 entry into cachedMatches
  • Lifecycle-hook identity uses routeId only via new hookExitingMatches/hookEnteringMatches/hookStayingMatches variables, so onStay fires for same-route param changes

Sheraff's test from #6772 should now pass with this change — the previous match gets cached and reused within staleTime as expected.

Copy link
Member

@SeanCassiere SeanCassiere left a comment

Choose a reason for hiding this comment

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

In terms of the events being fired, could we check to see whether there's testing in-place, to confirm that the router instance still emits the onBeforeNavigate and onResolved events on navigate?

In particular, to make sure we don't break Sentry's tracing integration - https://github.com/getsentry/sentry-javascript/blob/develop/packages/react/src/tanstackrouter.ts

- Add same-route match caching tests (from PR TanStack#6772 by Sheraff) to verify
  that switching lifecycle hooks to routeId does NOT break cache identity
  (which still correctly uses match.id = routeId + params + loaderDeps).
  Tests cover both loaderDeps variants and path param variants.

- Add onBeforeNavigate subscription event tests to address SeanCassiere's
  request to verify router-level events still fire correctly after this
  change. These events are used by integrations like Sentry's TanStack
  Router instrumentation for page-transition tracking.

All 1017 tests pass (29 test files, 0 type errors).
@sleitor
Copy link
Contributor Author

sleitor commented Feb 27, 2026

Addressed both remaining review requests in commit eb67444 — test coverage added to packages/router-core/tests/callbacks.test.ts:


@Sheraff — caching regression tests (from #6772)

Added the two tests from your test PR to verify that switching lifecycle hooks to routeId does NOT affect cache behavior:

  • keeps previous loaderDeps variant cached and reuses it within staleTime — navigates /foo?page=1/foo?page=2/foo?page=1 and confirms the loader is only called twice (page=1 entry is found in cachedMatches and reused)
  • keeps previous params variant cached and reuses it within staleTime — same for path params (/posts/$postId)

Both tests confirm that match.id (routeId + params + loaderDeps) is still the cache identity, while only the lifecycle-hook arrays (hookExiting/Entering/StayingMatches) use routeId.


@SeanCassiereonBeforeNavigate event tests

Added three tests for the onBeforeNavigate subscription event:

  1. Fires when navigating to a new route
  2. Fires on every navigation including same-route loaderDeps changes (important for Sentry tracing — each param change should still be tracked)
  3. Includes toLocation and pathChanged in the event payload

Re: onResolved — this event is emitted from framework-specific Transitioner components (React/Vue/Solid), not from router-core directly. Existing tests for onResolved live in packages/react-router/tests/router.test.tsx, packages/vue-router/tests/router.test.tsx, and packages/solid-router/tests/router.test.tsx — those are unaffected by this PR since router.ts does not touch that code path.


Full suite: 1017 tests pass, 0 type errors (29 test files).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/router-core/tests/callbacks.test.ts (1)

43-73: Extract shared /foo + loaderDeps router setup to reduce duplication.

setupWithLoaderDeps and setupWithLoaderDepsForCaching duplicate route construction and loaderDeps logic; consolidating lowers drift risk in future lifecycle/caching tests.

♻️ Proposed refactor
+const pageLoaderDeps = ({ search }: { search: Record<string, unknown> }) => ({
+  page: search['page'],
+})
+
+const createFooRouter = (opts: {
+  onEnter?: () => void
+  onLeave?: () => void
+  onStay?: () => void
+  loader?: () => unknown
+  staleTime?: number
+} = {}) => {
+  const rootRoute = new BaseRootRoute({})
+  const fooRoute = new BaseRoute({
+    getParentRoute: () => rootRoute,
+    path: '/foo',
+    loaderDeps: pageLoaderDeps,
+    onEnter: opts.onEnter,
+    onLeave: opts.onLeave,
+    onStay: opts.onStay,
+    loader: opts.loader,
+    staleTime: opts.staleTime,
+    gcTime: opts.staleTime,
+  })
+  return new RouterCore({
+    routeTree: rootRoute.addChildren([fooRoute]),
+    history: createMemoryHistory(),
+  })
+}

Also applies to: 167-190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/tests/callbacks.test.ts` around lines 43 - 73, The test
helper duplication: consolidate the duplicated `/foo` route + loaderDeps
construction by extracting a single reusable function (e.g.,
setupWithLoaderDeps) that builds BaseRootRoute, BaseRoute (with getParentRoute,
path '/foo', loaderDeps, onEnter/onLeave/onStay), produces routeTree and returns
a RouterCore; then update the other helper (setupWithLoaderDepsForCaching) and
any tests that used the old duplicate logic to call this single helper
(optionally parameterize loaderDeps values or callbacks) so all tests reference
the same construction (look for symbols setupWithLoaderDeps,
setupWithLoaderDepsForCaching, BaseRootRoute, BaseRoute, loaderDeps, routeTree,
RouterCore).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/router-core/tests/callbacks.test.ts`:
- Around line 43-73: The test helper duplication: consolidate the duplicated
`/foo` route + loaderDeps construction by extracting a single reusable function
(e.g., setupWithLoaderDeps) that builds BaseRootRoute, BaseRoute (with
getParentRoute, path '/foo', loaderDeps, onEnter/onLeave/onStay), produces
routeTree and returns a RouterCore; then update the other helper
(setupWithLoaderDepsForCaching) and any tests that used the old duplicate logic
to call this single helper (optionally parameterize loaderDeps values or
callbacks) so all tests reference the same construction (look for symbols
setupWithLoaderDeps, setupWithLoaderDepsForCaching, BaseRootRoute, BaseRoute,
loaderDeps, routeTree, RouterCore).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8be9de and eb67444.

📒 Files selected for processing (1)
  • packages/router-core/tests/callbacks.test.ts

@sleitor
Copy link
Contributor Author

sleitor commented Feb 27, 2026

Addressed CodeRabbit's nitpick: consolidated setupWithLoaderDeps and setupWithLoaderDepsForCaching into a single createFooRouter(opts) helper (commit 40e7176). Less duplication, easier to extend.

@github-actions
Copy link

github-actions bot commented Feb 27, 2026

Bundle Size Benchmarks

  • Commit: d958ef18d84e
  • Measured at: 2026-02-27T20:47:56.741Z
  • Baseline source: history:d958ef18d84e
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 86.58 KiB +6 B (+0.01%) 272.45 KiB 75.22 KiB ▁▁▂▂▂▂▂▂▂▇▇█
react-router.full 89.61 KiB +9 B (+0.01%) 282.78 KiB 77.90 KiB ▂▂▁▁▁▂▂▂▂▇▇█
solid-router.minimal 35.88 KiB +12 B (+0.03%) 107.56 KiB 32.26 KiB ▁▁▂▂▂▅▅▅▅▇▇█
solid-router.full 40.21 KiB +11 B (+0.03%) 120.61 KiB 36.13 KiB ▁▁▃▃▃▅▅▅▅▇▇█
vue-router.minimal 51.75 KiB +14 B (+0.03%) 147.54 KiB 46.50 KiB ▁▁▂▂▂▅▅▅▅▇▇█
vue-router.full 56.55 KiB +13 B (+0.02%) 163.12 KiB 50.86 KiB ▁▁▂▂▂▅▅▅▅▇▇█
react-start.minimal 99.11 KiB +14 B (+0.01%) 311.58 KiB 85.68 KiB ▁▁▂▂▂▂▂▂▂▆▆█
react-start.full 102.49 KiB +20 B (+0.02%) 321.36 KiB 88.62 KiB ▁▁▂▂▂▂▂▂▂▆▆█
solid-start.minimal 48.19 KiB +11 B (+0.02%) 145.13 KiB 42.67 KiB ▁▁▃▃▃▅▅▅▅▇▇█
solid-start.full 53.67 KiB +12 B (+0.02%) 161.05 KiB 47.33 KiB ▁▁▂▂▂▅▅▅▅▇▇█

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

Comment on lines 2431 to 2437
@@ -2426,6 +2436,23 @@ export class RouterCore<
previousMatches.some((d) => d.id === match.id),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't use those anymore, do we?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right — and are no longer used after the split. Only is still needed (for populating ). Removed both in the latest commit.

After splitting match identity into cache-level (match.id) and
lifecycle-hook-level (match.routeId), enteringMatches and stayingMatches
became unused. Only exitingMatches is still needed for cachedMatches.

Addresses review comment from @Sheraff
@Sheraff Sheraff merged commit 442ada1 into TanStack:main Feb 27, 2026
8 checks passed
@sleitor sleitor deleted the fix/router-lifecycle-use-routeid branch February 27, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

onLeave/onEnter fire instead of onStay when loaderDeps change from search param updates

4 participants