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-router 1.170.29 / @tanstack/react-start 1.168.46 (the quoted code is also present in latest router-core 1.171.24)
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
beforeLoadreturns.contextualize(router-coredist/esm/load-client.js) updates the in-flight lane match in two steps: it assignsmatch.context = { ...parentContext, ...routeContext }before awaiting the route'sbeforeLoad, and merges the result back in only after the await. MeanwhileofferPendingfires atpendingMsand presents shallow clones of those same lane matches. If the timer lands inside the await, the presented root match still hasstatus: "success"(it sits above the pending boundary), so the root component re-renders from it — anduseRouteContext()is suddenly missing everythingbeforeLoadprovides.It only needs three ordinary ingredients: a pending component on the navigation target (or a
defaultPendingComponent), apendingMsshorter than an async rootbeforeLoad(we usedefaultPendingMs: 150; on mobile devices a 150ms+beforeLoadis routine), and a component that reads that context during render. Withrouter.invalidate()involved it's worse — invalidated matches present with zero delay (match.invalid ? 0 : pendingMs), so even a fastbeforeLoadwindow 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 disablesdefaultPreload: 'intent'— hover preloading moves the navigation outside the window, which is also why this skews hard to touch devices in production. Theold-versions-cleanbranch 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
beforeLoadresult 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
errorComponentfor a moment, and the navigation then completes. Consumers that cache the value are broken past the flash — in our app the locale from rootbeforeLoadseeds 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.contextbeforebeforeLoadsettles — keep the fresh merge local and assign once, after the result is in (assign directly only on the no-beforeLoadpath). Retained matches then keep their previous complete context while reloading, and fresh matches below the pending boundary are presented aspending, so nothing reads their empty context. Happy to open a PR with this plus a test if you want it.Platform
@tanstack/react-router1.170.29 /@tanstack/react-start1.168.46 (the quoted code is also present in latest router-core 1.171.24)