Skip to content

Bug: hydration completes in Header before page renders #6191

@daveycodez

Description

@daveycodez

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.

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