Skip to content

Hydration renders the document root with beforeLoad-provided context stripped when the root fails commitment #8128

Description

@antur84

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions