Mount-time setState in a layout-level client ancestor forces client render of a pending streamed Suspense boundary (no error; startTransition doesn't help)
Link to the code that reproduces this issue
https://github.com/team-hapcho/next-streaming-remount-repro
To Reproduce
npm install && npm run build && npm start(production build, port 4801)- In another terminal:
npm run probe— a Playwright script that opens each route with a unique query string and samples theuseId()prefixes of elements every 40 ms aftercommit.
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/AWhich 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).
Source: vercel/next.js