#99064·Next.js

Mount-time setState in a layout-level client ancestor forces client render of a pending streamed Suspense boundary (no error; startTransition doesn't help)

Author: team-hapchoCreated Sep 22, 2026Updated Sep 28, 2026
LabelsRuntime

Link to the code that reproduces this issue

https://github.com/team-hapcho/next-streaming-remount-repro

To Reproduce

  1. npm install && npm run build && npm start (production build, port 4801)
  2. In another terminal: npm run probe — a Playwright script that opens each route with a unique query string and samples the useId() prefixes of elements every 40 ms after commit.

Each page has a sibling loading.tsx, awaits ~400 ms, and renders a client component that calls useId() (a Radix Select; a plain <span id={useId()}> behaves the same).

Current vs. Expected behavior

Current: when a client component that is an ancestor of the streamed Suspense boundary (a context provider in the root layout wrapping {children}) calls setState on mount — while the boundary is still pending — React discards the boundary's server-rendered HTML once it arrives and mounts its client components fresh: useId() yields client ids (_r_…) instead of server ids (_R_…), and for ~100–800 ms the old server-rendered elements and the new client-rendered ones coexist in the DOM (visible flash; strict-mode Playwright locators see duplicates). No recoverable error is reported (verified by patching onRecoverableError), no navigation, no console output.

Route Layout Result
/p3 provider around {children}, no setState hydrated (_R_ ids kept)
/p1 provider calls setState in useEffect on mount client-rendered (_r_)
/p2 same, wrapped in startTransition client-rendered (_r_)
/p4 same setState, 2 s later (after streaming finished) hydrated
/p5 provider placed inside the page (below the boundary) hydrated
/p6 client sibling (not an ancestor) calls setState on mount hydrated

Expected: the pending boundary should hydrate its streamed server HTML once it arrives and then receive the update; at minimum, an update wrapped in startTransition should not force the boundary to client-render (per the React docs on transitions and not-yet-hydrated boundaries). If this is intended, it would be worth documenting: any root-layout provider that reads localStorage/fetches on mount (a very common pattern) silently turns every streamed boundary below it into a client render.

Reproduced on React 19.3.0 and 19.2.7 (react/react-dom swapped in the repro), Next 16.2.10 and 16.3.6, with and without cacheComponents. Found in production: a root-layout provider hydrating a list from localStorage caused every cache-miss render of a search page (loading.tsx + searchParams) to remount its client components.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.4.0
Binaries:
  Node: 26.0.0
  npm: 12.0.2
  pnpm: 10.33.0
Relevant Packages:
  next: 16.3.6
  react: 19.3.0
  react-dom: 19.3.0
  typescript: 7.0.2
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Partial Prerendering (PPR), Runtime, Streaming

Which stage(s) are affected? (Select all that apply)

next build (local), next start (local)

Additional context

Workaround we use: no streaming boundary under such providers (page blocks until data), or providers with no mount-time React state (external store + useSyncExternalStore in consumers).