Skip to content

Commit 6cf5885

Browse files
authored
fix: useId() mismatch between SSR and client side (#752)
### Context: The Previous Rendering Architecture Previously, the framework used a single, nested rendering pass on the server to produce the initial HTML document. The user's `<Document>` component (containing the `<html>`, `<head>`, etc.) was rendered using React's standard Server-Side Rendering (SSR). As part of this same render, the framework would resolve the React Server Component (RSC) payload for the page and render its contents into the document shell. ### Problem: Non-Deterministic `useId` Generation This approach created a hydration mismatch for client components that rely on `React.useId` (such as those in Radix UI). React's hydration for `useId` requires deterministic rendering—the sequence of hook calls that generate IDs must be identical on the server and the client. Our single-pass architecture broke this determinism. The server would first traverse and render the components within the `<Document>` shell, advancing React's internal `useId` counter. Only then would it proceed to render the actual application components. The client, however, only hydrates the application content within the document, starting with a fresh `useId` counter. This discrepancy meant the server was performing extra rendering work that the client was unaware of, leading to a mismatch in the final IDs (e.g., server `_R_76_` vs. client `_r_0_`). This caused React to discard the server-rendered DOM, breaking interactivity and negating the benefits of SSR. ### Solution: Isolate, Render, and Stitch The solution was to re-architect the server-side rendering pipeline to enforce context isolation. The new "Nested Renders with Stream Stitching" model works as follows: 1. **Isolated Renders**: Instead of one nested render, we now perform two completely separate and concurrent renders on the server: - One for the application content, which generates an HTML stream (`appHtmlStream`). This guarantees it renders in a clean context with a fresh `useId` counter. - One for the `<Document>` shell, which generates another HTML stream (`documentHtmlStream`) containing a placeholder comment. 2. **Stream Stitching**: A custom utility merges these two streams on the fly. It streams the document shell until it finds the placeholder, at which point it injects the application's complete HTML stream before continuing with the rest of the document. This approach guarantees that the application content is rendered in an isolated context, ensuring the `useId` sequence generated on the server is identical to the one generated on the client during hydration, while at the same time ensuring streaming isn't blocked for both the document and app RSC renders. An important secondary benefit of this change is that the user-defined `<Document>` is now a true React Server Component. This aligns with developer expectations and unlocks the full power of the RSC paradigm (e.g., using `async/await` for data fetching, accessing server-only APIs) directly within the document shell, which was not possible before. The full details of this new architecture are captured in the updated [Hybrid Rendering documentation](<./docs/architecture/hybridRscSsrRendering.md>).
1 parent f799b9e commit 6cf5885

File tree

78 files changed

+19195
-366
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+19195
-366
lines changed

.notes/justin/worklogs/2025-0-17-dependency-greenkeeping-strategy.md

Lines changed: 0 additions & 65 deletions
This file was deleted.

.notes/justin/worklogs/2025-0.9-17-dependency-greenkeeping-strategy.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)