Repository navigation
useRouteContext() renders with beforeLoad-provided context stripped during pending presentation #8115
Description
Which project does this relate to?
Router
Describe the bug
Since the lane loader rewrite (1.170.29+), a client-side navigation can render an already-mounted route with a context that's missing every key its beforeLoad returns.
contextualize (router-core dist/esm/load-client.js) updates the in-flight lane match in two steps: it assigns match.context = { ...parentContext, ...routeContext } before awaiting the route's beforeLoad, and merges the result back in only after the await. Meanwhile offerPending fires at pendingMs and presents shallow clones of those same lane matches. If the timer lands inside the await, the presented root match still has status: "success" (it sits above the pending boundary), so the root component re-renders from it — and useRouteContext() is suddenly missing everything beforeLoad provides.
It only needs three ordinary ingredients: a pending component on the navigation target (or a defaultPendingComponent), a pendingMs shorter than an async root beforeLoad (we use defaultPendingMs: 150; on mobile devices a 150ms+ beforeLoad is routine), and a component that reads that context during render. With router.invalidate() involved it's worse — invalidated matches present with zero delay (match.invalid ? 0 : pendingMs), so even a fast beforeLoad window gets rendered.
Your Example Website or App
https://github.com/antur84/tanstack-router-pending-context-loss-repro
pnpm install && pnpm build && pnpm start, open the app with the console showing, click either button. One attempt per page load (a retained destination never goes pending), and note the repro disables defaultPreload: 'intent' — hover preloading moves the navigation outside the window, which is also why this skews hard to touch devices in production. The old-versions-clean branch is the identical app pinned to 1.170.15 / 1.168.25: same clicks, no context loss.
Expected behavior
A match being reloaded keeps its previous (complete) context visible until the new beforeLoad result is in — the rendered UI should never observe the half-built context.
Actual behavior
The root component re-renders with the stripped context, crashes into the root errorComponent for a moment, and the navigation then completes. Consumers that cache the value are broken past the flash — in our app the locale from root beforeLoad seeds the i18n runtime, so this crashed ~500 users/hour to the error screen after we upgraded (reverting stopped it).
Suggested fix
Don't overwrite match.context before beforeLoad settles — keep the fresh merge local and assign once, after the result is in (assign directly only on the no-beforeLoad path). Retained matches then keep their previous complete context while reloading, and fresh matches below the pending boundary are presented as pending, so nothing reads their empty context. Happy to open a PR with this plus a test if you want it.
Platform
- OS: any (reproduced on macOS; production impact on Android/iOS)
- Browser: any
- Version:
@tanstack/react-router1.170.29 /@tanstack/react-start1.168.46 (the quoted code is also present in latest router-core 1.171.24)
Activity
antur84 commentedon Aug 19, 2026
Verified against 1.170.30 / router-core 1.171.25: the repro no longer crashes in any mode — #8084's
retainedEndgating means nothing presents during a retained match's beforeLoad, and I could not reproduce via plain navigation, invalidate+navigate, or a stale post-hydration reload. Closing as fixed by #8084.One note for posterity: the two-step
match.contextassignment incontextualizeis still present, so the suggested hardening (assign context once, after beforeLoad settles) would close the window structurally rather than relying on presentation timing — happy to PR that if you'd take it, otherwise this is resolved for us.antur84 commentedon Aug 20, 2026
Follow-up: on 1.170.30 / router-core 1.171.25 the reported
pendingMsvector is fixed, but the underlying window is still reachable through a second vector — a synchronous flush rendering the live match during a same-id reload, no pending presentation involved — at ~0.5% of the original rate in our production traffic. Field data and stack analysis in #8116 (comment); #8116 closes the window itself rather than a vector.bump TanStack Router and Start past the retained-context fix
Give the signed-in app one shell instead of one per section (#694)