TanStack / TanStack/router

Bug: hydration completes in Header before page renders

Open
#6,191 19 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

package: react-start
Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

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:

  • true in the Header
  • false in 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:

  1. bun install
  2. nx dev start-heroui-example
  3. Sign up a random account (no db required)
  4. Go to settings
  5. Observe hydration error
Expected behavior

Hydration should not complete on the RootComponent before a Child SSR route.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked start-heroui-example reproduction: run bun install and nx dev start-heroui-example, then inspect the shellComponent/RootDocument, Header, page route, and useHydrated() usage described in the issue. Trace how hydration state is shared between the shell and child SSR route. Done means Header and page observe one monotonic hydration lifecycle and the settings page receives client-side updates without the workaround.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend, full-stack
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.