Which project does this relate to?
Router
Describe the bug
hydrate() (router-core load-client.ts) merges a dehydrated match's beforeLoad context (b) only for committed matches. A root that fails commitment — the dehydrated match id doesn't equal the client-rebuilt id (a URL rewrite/serialization disagreement between server and client), or the dehydrated entry is pending/stale — gets context = { ...routerContext, ...routeContext } with every beforeLoad-provided key missing.
That stripped context then gets rendered: the pending document root can't be replaced by pending UI (Match.tsx: "Replacing an SSR document root with pending UI would remove <html>. Hydrated matches retain their prior data, so keep rendering it"), so the root's real component renders and useRouteContext() is missing everything beforeLoad provides. The comment's assumption is exactly what fails — an uncommitted root has no prior data.
This is distinct from #8115: that issue is the contextualize lane window during client-side navigation (vectors addressed by #8084 and #8116). This one drops the context a phase earlier, at the hydration merge — the repro below fails identically on main and on #8116's branch.
Your Example Website or App
Deterministic regression test in the linked PR (packages/react-router/tests/issue-8115-hydration-context-window.test.tsx): dehydrate a root with an id skew plus b: { locale: 'en' }, render while hydration is in flight, and the root's real component renders with useRouteContext().locale === undefined before recovering (['undefined', 'en']).
Expected behavior
A hydrated root never renders with a context missing its beforeLoad-provided keys — either the dehydrated context is available to the render, or the real component is withheld until the client beforeLoad settles.
Actual behavior
The root component renders with the stripped context and, in our app, crashes into the root errorComponent (the locale from root beforeLoad seeds the i18n runtime — same production symptom as #8115). After upgrading to 1.170.30 / router-core 1.171.25 we still measured it at ~2–3 events/hour, concentrated in bursts right after each deploy (stale tabs reloading → a wave of fresh hydrations), overwhelmingly on mobile browsers hydrating our ssr: false shell route — i.e., precisely where hydration windows are widest.
Suggested fix
PR attached: track beforeLoad settlement on the client — contextualize records __beforeLoadContext on settle (mirroring load-server), hydrate() restores it from the dehydrated b for committed matches — and gate the document-root pending exception on it, so a root whose beforeLoad has never contributed presents pending instead of rendering with missing context keys.
Platform
- OS: any (production impact concentrated on Android/mobile)
- Version:
@tanstack/react-router 1.170.30 / @tanstack/router-core 1.171.25 (repro verified on current main)
Which project does this relate to?
Router
Describe the bug
hydrate()(router-coreload-client.ts) merges a dehydrated match's beforeLoad context (b) only for committed matches. A root that fails commitment — the dehydrated match id doesn't equal the client-rebuilt id (a URL rewrite/serialization disagreement between server and client), or the dehydrated entry is pending/stale — getscontext = { ...routerContext, ...routeContext }with every beforeLoad-provided key missing.That stripped context then gets rendered: the pending document root can't be replaced by pending UI (
Match.tsx: "Replacing an SSR document root with pending UI would remove<html>. Hydrated matches retain their prior data, so keep rendering it"), so the root's real component renders anduseRouteContext()is missing everythingbeforeLoadprovides. The comment's assumption is exactly what fails — an uncommitted root has no prior data.This is distinct from #8115: that issue is the
contextualizelane window during client-side navigation (vectors addressed by #8084 and #8116). This one drops the context a phase earlier, at the hydration merge — the repro below fails identically onmainand on #8116's branch.Your Example Website or App
Deterministic regression test in the linked PR (
packages/react-router/tests/issue-8115-hydration-context-window.test.tsx): dehydrate a root with an id skew plusb: { locale: 'en' }, render while hydration is in flight, and the root's real component renders withuseRouteContext().locale === undefinedbefore recovering (['undefined', 'en']).Expected behavior
A hydrated root never renders with a context missing its beforeLoad-provided keys — either the dehydrated context is available to the render, or the real component is withheld until the client
beforeLoadsettles.Actual behavior
The root component renders with the stripped context and, in our app, crashes into the root
errorComponent(the locale from rootbeforeLoadseeds the i18n runtime — same production symptom as #8115). After upgrading to 1.170.30 / router-core 1.171.25 we still measured it at ~2–3 events/hour, concentrated in bursts right after each deploy (stale tabs reloading → a wave of fresh hydrations), overwhelmingly on mobile browsers hydrating ourssr: falseshell route — i.e., precisely where hydration windows are widest.Suggested fix
PR attached: track beforeLoad settlement on the client —
contextualizerecords__beforeLoadContexton settle (mirroringload-server),hydrate()restores it from the dehydratedbfor committed matches — and gate the document-root pending exception on it, so a root whose beforeLoad has never contributed presents pending instead of rendering with missing context keys.Platform
@tanstack/react-router1.170.30 /@tanstack/router-core1.171.25 (repro verified on currentmain)