Skip to content

<Hydrate when={never()}> renders nothing when it mounts after client-side navigation #8668

Open

Description

@pfurini

Which project does this relate to?

Start

Describe the bug

A <Hydrate when={never()}> boundary that first mounts during client-side navigation stays empty. Its children never render, the deferred chunk is never requested, and nothing is logged.

The deferred hydration guide says:

If the same boundary mounts later during client-side navigation, it renders normally because there is no initial server HTML to preserve.

A full page load works as documented: the server HTML stays static.

Complete minimal reproducer

https://github.com/pfurini/start-hydrate-never-navigation

Steps to Reproduce the Bug

  1. pnpm install && pnpm dev
  2. Open http://localhost:3000/deferred directly: "Deferred content" is shown.
  3. Open http://localhost:3000/ and click "Deferred".
  4. <main> now contains only <div data-ts-hydrate-when="never"></div>.

Expected behavior

After client-side navigation, the boundary renders its children, as the guide describes.

Screenshots or Videos

No response

Platform

  • Router / Start Version: @tanstack/react-start 1.168.61 (@tanstack/react-start-client 1.168.40), @tanstack/react-router 1.170.42
  • OS: macOS 15.7.4
  • Browser: Chromium (Playwright, headless)
  • Browser Version: 149.0.7827.55
  • Bundler: vite
  • Bundler Version: 8.3.4

Additional context

NeverGate (packages/react-start-client/src/hydration/never.tsx) suspends on neverPromise on every client render, including mounts where no server HTML was preserved. The marker ref also calls replaceChildren() in that case.

Gating both on shouldPreserveServerHTMLRef.current fixed it in a local patch of the published dist/esm/hydration/never.js:

 function NeverGate(props) {
   if (isServer ?? typeof window === "undefined") return props.children;
+  if (!props.preserve) return props.children;
   if (!reactUse) throw neverPromise;
 ...
-  if (!shouldPreserveServerHTMLRef.current) element.replaceChildren();
-  else saveFallbackHtml(id, element);
+  if (shouldPreserveServerHTMLRef.current) saveFallbackHtml(id, element);
 ...
-  jsx(NeverGate, { children: props.children })
+  jsx(NeverGate, { preserve: shouldPreserveServerHTMLRef.current, children: props.children })

I don't know what the replaceChildren() call guards against, so this may be incomplete. Happy to open a PR with a regression test.

AI disclosure: drafted with an AI assistant (Claude), which also built and ran the reproducer. I reviewed it before posting.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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