-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Which project does this relate to?
Start
Describe the bug
Bug: useHydrated inconsistent across Root shellComponent and page routes (hydration completes in Header before page)
Description
I’m running into a serious hydration inconsistency when using TanStack Start (SSR) with a shellComponent (RootDocument) that renders a <Header />.
The issue is that hydration appears to complete inside the Header before the page route hydrates, causing useHydrated() to return:
truein the Headerfalsein the page component after hydration already occurred
Once hydration becomes true, it should never flip back to false, but that’s exactly what’s happening.
This causes real-world bugs where:
- Data fetching hooks complete in the Header
- Pages render with SSR state (
isPending: true) - Pages never receive the client-side update
- Inputs remain permanently disabled, skeletons never disappear, etc.
Code Samples
Root Route (shellComponent)
export const Route = createRootRoute({
shellComponent: RootDocument,
})
function RootDocument({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body>
<Providers>
<Header />
{children}
</Providers>
<Scripts />
</body>
</html>
)
}
Header (hydration becomes true early)
export function Header() {
const { isPending } = authClient.useSession()
const isHydrated = useHydrated()
console.log("Header:", { isHydrated, isPending })
return (...)
}
Page Route (useHydrated becomes false again)
function SettingsPage() {
const isHydrated = useHydrated()
console.log("Page:", { isHydrated })
return <Settings />
}⸻
Actual Behavior
• Header logs isHydrated: true
• Page logs isHydrated: false
• Page never receives client-side updates from hooks
• SSR state (e.g. isPending: true) is permanently stuck
Example real-world workaround (required to avoid broken UI):
disabled={isPending || !isHydrated}
This should not be necessary.
⸻
Expected Behavior
• Hydration should be global and monotonic
• Once isHydrated === true, it should never revert
• Header and page should observe the same hydration lifecycle
• Data fetched in shell components should not “complete early” relative to page hydration
⸻
Why This Is Severe
This breaks:
• Pending states
• Disabled inputs
• Auth/session-based layouts
• Any app that fetches data in a shared shell component
It makes SSR apps behave inconsistently and causes bugs that are extremely difficult to diagnose.
⸻
Suspected Cause
It feels like:
• The shellComponent is being hydrated in a separate pass / chunk
• Page routes are still treated as “not hydrated” afterward
• Hydration context is not shared correctly between shell and route components
⸻
Notes
Happy to reduce this further if needed, but this repro already demonstrates the issue clearly.
Thanks for taking a look 🙏
Your Example Website or App
https://github.com/better-auth-ui/better-auth-ui/tree/v4/examples/start-heroui-example
Steps to Reproduce the Bug or Issue
Reproduction
Minimal reproduction repo:
🔗 https://github.com/better-auth-ui/better-auth-ui/tree/v4/examples/start-heroui-example
Steps:
- bun install
- nx dev start-heroui-example
- Sign up a random account (no db required)
- Go to settings
- Observe hydration error
Expected behavior
Hydration should not complete on the RootComponent before a Child SSR route.