AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Mount-Time SetState In Layout-Level Client Ancestor Forces Client Render Of Pending Streamed Suspense Boundary

When a client ancestor of a streamed Suspense boundary (e.g., a root layout provider) calls setState on mount while the boundary is still pending, React discards the server-rendered HTML and mounts the boundary as client-rendered, causing visible flash and duplicate elements. startTransition does not mitigate the issue.

highConfidence 88%Next.jsAffected Vreact@19.2.7Affected Vreact@19.3.0Affected Vnext@16.2.10Affected Vnext@16.3.6

Origin Analysis

React's concurrent hydration logic treats any state update from an ancestor during the pending phase of a Suspense boundary as a reason to abandon hydration and fall back to client rendering. The update is processed with default (sync) priority, even when wrapped in startTransition, and the scheduler does not defer it until after the boundary completes hydration. This is a design flaw in React's coordination between transitions and not-yet-hydrated Suspense boundaries.
1. Create a Next.js app with a root layout containing a client provider that calls setState in useEffect on mount (simulating reading localStorage or fetching). 2. Add a page with loading.tsx and a streamed Suspense boundary that renders a client component using useId(). 3. Build and start the production server, then navigate to the page with a unique query string. 4. Observe that the useId() prefix changes from _R_ (server) to _r_ (client), indicating client render, and duplicated DOM elements during transition.

Fixing Code Block

Edge Case Audit

Using useSyncExternalStore requires careful store design: getSnapshot must return a cached value that does not change on every call, otherwise infinite re-renders occur. The subscribe function must return a cleanup to avoid memory leaks. In StrictMode, subscribe is called twice, so listeners must be idempotent. Avoid updating the store during render (e.g., in getSnapshot) as it triggers warnings. Concurrent updates may cause temporary tearing if not using immutable state. Rollback to useState+useEffect will reintroduce the issue until React fixes the underlying coordination.

Ecosystem Topology