Skip to content

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Jan 16, 2026

Summary by CodeRabbit

  • New Features

    • Navigation operations are now asynchronous and return a result indicating success or blocked.
    • Navigation state now includes cached route-match snapshots to speed back/forward navigation.
  • Bug Fixes

    • Prevents unnecessary route reloads for internal navigations via a new skip-load guard.
  • Tests

    • Test suites updated to await asynchronous navigation and DOM updates.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Adds async navigation results and navigate options propagation, introduces match-snapshot caching and session markers for fast-path back/forward navigations, updates Transitioner to optionally skip loads, and updates tests to await asynchronous navigation timing.

Changes

Cohort / File(s) Summary
Async History API
packages/history/src/index.ts
Introduced NavigationResult type; push/replace now return Promise<NavigationResult>; tryNavigation/notify propagate navigateOpts and return navigation result
Subscriber / NavigateOptions
packages/history/src/index.ts
Made SubscriberArgs public and added skipTransitionerLoad?: boolean to NavigateOptions; notify forwards navigateOpts to subscribers
Parsed History State
packages/history/src/index.ts
Added __TSR_matches, __TSR_resetScroll, and __TSR_sessionId to history state payloads
Match Snapshot Types
packages/router-core/src/location.ts, packages/router-core/src/index.ts
Added ValidatedSearchEntry and MatchSnapshot types; ParsedLocation._matchSnapshot? property; export surface adjusted to include new types
Router Core Snapshot Logic
packages/router-core/src/router.ts
Added snapshot-driven fast-path matching, buildMatchSnapshot/buildMatchSnapshotFromRoutes, MatchRoutesOpts.snapshot, sessionId handling, and propagation of snapshots into history state; load signature gained _skipUpdateLatestLocation
Transitioner Load Guard
packages/react-router/src/Transitioner.tsx
Router.history subscriber now inspects navigateOpts?.skipTransitionerLoad to conditionally skip router.load()
Tests — Awaiting Async Navigation
packages/react-router/tests/*, packages/solid-router/tests/*, packages/vue-router/tests/*, packages/router-core/tests/load.test.ts
Tests updated to await navigation completion (use await act(), waitFor, microtask awaits); some expected update counts/timings adjusted
Generated Route Types Adjustments
e2e/.../src/routeTree.gen.ts (multiple)
Tweaked generated fullPath typings for some layout/redirect routes (e.g., '''/', '/redirect''/redirect/')

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant History as RouterHistory
    participant Router as Router Core
    participant Matcher as Route Matcher

    Client->>History: push(path, state, navigateOpts)
    activate History
    History->>History: tryNavigation() -> Promise<NavigationResult>
    alt navigateOpts.skipTransitionerLoad true
        History->>Router: commit location without triggering Transitioner load
    else
        History->>Router: build/commit location and trigger load()
    end
    activate Router
    alt snapshot available and valid
        Router->>Matcher: reuse MatchSnapshot (fast-path)
    else
        Router->>Matcher: perform full matchRoutes()
    end
    Matcher-->>Router: matchResult or reused snapshot
    Router-->>History: commit location (with __TSR_matches/__TSR_sessionId)
    deactivate Router
    alt Navigation blocked by blocker
        History-->>Client: Promise resolves { type: "BLOCKED" }
    else
        History-->>Client: Promise resolves { type: "SUCCESS" }
    end
    deactivate History
Loading
sequenceDiagram
    participant Browser as Browser (popstate)
    participant History as RouterHistory
    participant Snapshot as MatchSnapshot Cache
    participant Matcher as Route Matcher

    Browser->>History: popstate (back/forward)
    activate History
    alt __TSR_matches present and session valid
        History->>Snapshot: read __TSR_matches / _matchSnapshot
        Snapshot-->>History: snapshot data
        History->>Matcher: reuse snapshot (skip full matching)
    else
        History->>Matcher: perform full matchRoutes()
    end
    Matcher-->>History: matchResult
    History-->>Browser: navigation complete
    deactivate History
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • nlynzaad
  • SeanCassiere

"🐰 I nibble code and stash a map,
Snapshots tucked in history's lap,
Async hops make navigation bright,
I skip a load and bound outright,
Breadcrumbs saved for back/forward flight! 🥕"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: don't reparse upon navigation' directly relates to the PR's core objective of implementing a snapshot-based fast-path routing mechanism to avoid reparsing routes during navigation, as evidenced by the addition of MatchSnapshot types, buildMatchSnapshot functions, and snapshot-driven caching throughout the router-core and history packages.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@nx-cloud
Copy link

nx-cloud bot commented Jan 16, 2026

View your CI Pipeline Execution ↗ for commit 4b0a7fe

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

☁️ Nx Cloud last updated this comment at 2026-01-19 01:13:56 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 16, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@6398

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@6398

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@6398

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@6398

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@6398

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@6398

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@6398

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@6398

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@6398

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6398

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@6398

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@6398

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@6398

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@6398

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@6398

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6398

@tanstack/vue-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-client@6398

@tanstack/vue-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-server@6398

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@6398

commit: 4b0a7fe

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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/router-core/src/router.ts`:
- Around line 1295-1313: The snapshot fast-path sets parsedParams = undefined
which causes a TypeError later when code checks "key in parsedParams" if
skipRouteOnParseError is enabled; change the snapshot branch to initialize
parsedParams to an empty object (e.g., parsedParams = {}) or otherwise ensure
parsedParams is always an object when snapshotValid is true so subsequent checks
in the router (referenced by parsedParams, skipRouteOnParseError, snapshotValid,
matchedRoutes, routeParams) won't throw.
- Around line 2933-2974: buildMatchSnapshot fails to include a configured custom
notFoundRoute in the returned snapshot, so when notFoundRoute is provided you
should set snapshot.globalNotFoundRouteId to that route's id; update
buildMatchSnapshot to check if notFoundRoute is truthy and assign
snapshot.globalNotFoundRouteId = notFoundRoute.id (before the fallback logic
that scans matchedRoutes or assigns rootRouteId) so snapshots include the custom
404 route for later matching.
🧹 Nitpick comments (1)
packages/history/src/index.ts (1)

227-256: Consider returning NavigationResult from go, back, and forward for API consistency.

These methods call tryNavigation but discard its result, while push and replace now return Promise<NavigationResult>. This creates an asymmetry in the API. For browser history, this may be intentional since navigation completes via popstate events, but callers cannot programmatically detect if these navigations were blocked.

Comment on lines 1295 to 1296
// Fast-path: use snapshot hint if valid (all route IDs must exist)
const snapshot = opts?.snapshot
const snapshotValid =
snapshot &&
snapshot.routeIds.length > 0 &&
snapshot.routeIds.every((id) => this.routesById[id])

let matchedRoutes: ReadonlyArray<AnyRoute>
let routeParams: Record<string, string>
let globalNotFoundRouteId: string | undefined
let parsedParams: Record<string, unknown> | undefined

if (snapshotValid) {
// Rebuild matched routes from snapshot
matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]!)
routeParams = { ...snapshot.params }
globalNotFoundRouteId = snapshot.globalNotFoundRouteId
parsedParams = undefined // Not available from snapshot, will re-parse if needed
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against undefined parsedParams in snapshot fast-path.

When skipRouteOnParseError is enabled, later logic uses key in parsedParams. With the snapshot path, parsedParams is set to undefined, which can throw a TypeError. Use an empty object (or a guard) to avoid crashes.

🐛 Proposed fix
-      parsedParams = undefined // Not available from snapshot, will re-parse if needed
+      // Not available from snapshot; keep empty to avoid undefined access later
+      parsedParams = {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fast-path: use snapshot hint if valid (all route IDs must exist)
const snapshot = opts?.snapshot
const snapshotValid =
snapshot &&
snapshot.routeIds.length > 0 &&
snapshot.routeIds.every((id) => this.routesById[id])
let matchedRoutes: ReadonlyArray<AnyRoute>
let routeParams: Record<string, string>
let globalNotFoundRouteId: string | undefined
let parsedParams: Record<string, unknown> | undefined
if (snapshotValid) {
// Rebuild matched routes from snapshot
matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]!)
routeParams = { ...snapshot.params }
globalNotFoundRouteId = snapshot.globalNotFoundRouteId
parsedParams = undefined // Not available from snapshot, will re-parse if needed
} else {
// Fast-path: use snapshot hint if valid (all route IDs must exist)
const snapshot = opts?.snapshot
const snapshotValid =
snapshot &&
snapshot.routeIds.length > 0 &&
snapshot.routeIds.every((id) => this.routesById[id])
let matchedRoutes: ReadonlyArray<AnyRoute>
let routeParams: Record<string, string>
let globalNotFoundRouteId: string | undefined
let parsedParams: Record<string, unknown> | undefined
if (snapshotValid) {
// Rebuild matched routes from snapshot
matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]!)
routeParams = { ...snapshot.params }
globalNotFoundRouteId = snapshot.globalNotFoundRouteId
// Not available from snapshot; keep empty to avoid undefined access later
parsedParams = {}
} else {
🤖 Prompt for AI Agents
In `@packages/router-core/src/router.ts` around lines 1295 - 1313, The snapshot
fast-path sets parsedParams = undefined which causes a TypeError later when code
checks "key in parsedParams" if skipRouteOnParseError is enabled; change the
snapshot branch to initialize parsedParams to an empty object (e.g.,
parsedParams = {}) or otherwise ensure parsedParams is always an object when
snapshotValid is true so subsequent checks in the router (referenced by
parsedParams, skipRouteOnParseError, snapshotValid, matchedRoutes, routeParams)
won't throw.

Comment on lines +1297 to +1283
const snapshotValid =
snapshot &&
snapshot.routeIds.length > 0 &&
snapshot.routeIds.every((id) => this.routesById[id])
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this check necessary? (mainly snapshot.routeIds.every((id) => this.routesById[id]))

Can we not trust what is passed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

theoretically if you reload you could get a new bundle served. this check tries to detect if the route tree changed. however it's not sufficient probably as the routes might still exist but would match otherwise.

Copy link
Contributor

Choose a reason for hiding this comment

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

this checks if all snapshot ids exist in the routes, is this sufficient in the case where there are new routes that hasn't been loaded in the snapshot yet? specifically, is this.routeIds.every((id) => snapshot.routesById[id]) not necessary?

const rewrittenUrl = executeRewriteOutput(this.rewrite, url)

// Use encoded URL path for href (consistent with parseLocation)
const encodedHref = url.href.replace(url.origin, '')
Copy link
Contributor

Choose a reason for hiding this comment

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

can probably just build this using url.pathname + url.search + url.hash here

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.

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
packages/router-core/src/router.ts (1)

1858-1891: href/url invariant breaks after output rewrite.
href is computed from the pre‑rewrite URL while url is stored as the rewritten (public) URL. This violates the ParsedLocation contract (href should be derived from url) and can desync pathname/href from url when rewrites change origin or path.

🐛 Proposed fix
       return {
         publicHref:
           rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
         href: encodedHref,
-        url: rewrittenUrl,
+        url,
         pathname: nextPathname,
🧹 Nitpick comments (2)
packages/history/src/index.ts (2)

34-43: Consider aligning go/back/forward with async results (or document the asymmetry).
With push/replace now returning Promise<NavigationResult>, consumers may expect similar affordances for other navigation methods. If you intend to keep them sync, a brief doc note would prevent confusion.


148-155: Expose navigateOpts on RouterHistory.notify for custom histories.
Since notify now forwards navigateOpts to subscribers, consider widening the public RouterHistory.notify signature to (action, navigateOpts?) so custom history implementations can propagate options without casting.

@schiller-manuel schiller-manuel merged commit 6cbd96e into main Jan 19, 2026
6 checks passed
@schiller-manuel schiller-manuel deleted the avoid-reparse branch January 19, 2026 22:00
@arekrgw
Copy link

arekrgw commented Jan 22, 2026

I think this exact PR broke my apps navigation. Before @tanstack/react-router@1.153.2 it worked and from this version it does not.

Details:

First when navigating to the article page I do it like that:
image

Then on my article page I have back button which uses back param as an url. Back param usually look like this: /stories?page=1&query=&limit=20
image

When I click on it, router shows 404 page, when I reload page then it shows correct page without any URL changes.

Should it be done differently from now on or something is off?

@s3rbug
Copy link

s3rbug commented Jan 22, 2026

my route params is always undefined after this PR changes

import { createRootRouteWithContext, createRoute } from '@tanstack/react-router';

const rootRoute = createRootRouteWithContext()();

const layoutRoute = createRoute({
    getParentRoute: () => rootRoute,
    id: 'layout',
    component: AppPageLayout,
});

const lobbyRoute = createRoute({
    getParentRoute: () => layoutRoute,
    path: '/',
}).lazy(() => import('./lobby/LobbyPage').then((m) => m.Route));

const itemsIndexRoute = createRoute({
    getParentRoute: () => layoutRoute,
    path: 'items',
}).lazy(() => import('./item/ItemsPage').then((m) => m.Route));

const itemRoute = createRoute({
    getParentRoute: () => rootRoute,
    path: 'items/view/$itemId',
    loader: (props) => {
        // props.params.itemId is undefined after @tanstack/react-router@1.153.2
    },
}).lazy(() => import('./item/ItemPage').then((m) => m.Route));

export const routeTree = rootRoute.addChildren([
  itemRoute,
  layoutRoute.addChildren([itemsIndexRoute]),
  lobbyRoute
]);

@iffa
Copy link

iffa commented Jan 23, 2026

Seeing a similar issue, our app with lots of search param usage now causes a "not found" every time search params change. Once you reload the page, it works.

@arekrgw
Copy link

arekrgw commented Jan 23, 2026

Seeing a similar issue, our app with lots of search param usage now causes a "not found" every time search params change. Once you reload the page, it works.

I've pulled newest changes today and it works now for me

Ignore me, it does not work, went back to stable version

@iffa
Copy link

iffa commented Jan 23, 2026

Seeing a similar issue, our app with lots of search param usage now causes a "not found" every time search params change. Once you reload the page, it works.

I've pulled newest changes today and it works now for me

Ignore me, it does not work, went back to stable version

Yep, version 1.154.13 still has the same issue, 1.153.1 is the latest one that works.

Sheraff added a commit that referenced this pull request Jan 23, 2026
Sheraff added a commit that referenced this pull request Jan 23, 2026
Revert "refactor: don't reparse upon navigation (#6398)"

This reverts commit 6cbd96e.
@schiller-manuel
Copy link
Contributor Author

we reverted this and will revisit this perf optimization at a later point

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.

7 participants