awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

Hydration mismatch: causes, detection, and the determinism rule

Crux How the hydration contract works, what breaks it, how React recovers, and the determinism rule that prevents CLS and re-render cost in production.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 18 min

A greeting component renders “Good morning” or “Good evening” based on the hour. In development it works. In production, users see a flash: text changes the instant the page loads, and layout shifts by 0.14. The browser console says: “Text content did not match. Server: ‘Good evening’ Client: ‘Good morning’.” The cause is a broken contract called a hydration mismatch.

The mechanism, precisely. Hydration is a contract: the DOM the client produces on its first render must be structurally identical to the DOM the server serialised into the HTML. React walks the server DOM and the client’s virtual tree in lockstep, adopting each node. When a node disagrees — different text, different attributes, a different element, an extra or missing child — the contract is broken. React’s recovery: it logs a mismatch error and, for the affected subtree, throws away the server HTML and client-renders it from scratch. That recovery render is the visible symptom: a flash of changed content and a layout shift that spikes CLS.

The common causes are all forms of non-determinism between the two environments: Date.now(), Math.random(), window/document/localStorage read during render (undefined on the server), Intl formatting that depends on locale or timezone, typeof window branches, and browser extensions that mutate the DOM before React hydrates.

The determinism rule. The cure is to make the first client render a pure function of the same inputs the server had. Any value that can differ between server and client must not appear during render — it is deferred to a useEffect that runs after hydration, or gated behind a useState(false) “mounted” flag flipped in an effect, so both the server render and the first client render produce the placeholder and only a later render introduces the client-specific value.

Hydration mismatch: what breaks and how

Non-determinism source
Why it mismatches
Fix
new Date() / Date.now()
Server and client run at different moments
Defer to useEffect; render null or server value first
Math.random()
Different random values each run
Generate server-side, pass as serialized prop
window / localStorage
undefined on server, object on client
typeof window check deferred to effect
Intl / timezone
Server timezone differs from user timezone
Pass timezone explicitly; format server-side
Browser extension DOM mutation
Extension adds/removes nodes before hydration
suppressHydrationWarning on affected nodes

The data mismatch trap. The same constraint applies to data. If the server renders from one snapshot of an API response and the client refetches a newer one before hydrating, the trees diverge — a hydration mismatch caused by data, not by time or locale. The fix: the server’s data must be embedded in the HTML and reused by the client for the first render. This is exactly what frameworks do with the dehydrated query cache (React Query, Apollo, SWR): serialize the server’s data into the HTML, rehydrate it on the client, render the first pass from that cache, and only then allow fresh fetches.

Edge cases

The size of the embedded dehydrated cache matters. A very large API response serialized into the HTML inflates the document and parse time — sometimes the right trade is to embed the minimum for the above-the-fold content and refetch the rest post-hydration, accepting one extra background fetch to keep the document small.

Streaming SSR internals. renderToPipeableStream renders the tree depth-first and flushes the shell — everything outside any pending Suspense boundary — as soon as it is ready. For each Suspense boundary whose data has not resolved, the server emits the fallback inline and keeps the connection open. When a boundary’s data resolves, the server renders that boundary’s real content and writes it as a chunk: a hidden <div> carrying the content plus a tiny inline <script> that relocates it into the right slot and removes the fallback. The browser, parsing HTML progressively, executes that script the moment the chunk arrives. Boundaries stream in completion order, not document order — a fast widget low on the page can arrive before a slow widget above it.

Streaming and hydration interleave. As each Suspense boundary’s HTML streams in, React on the client can hydrate that boundary independently — it does not wait for the whole document. If the user interacts with an already-streamed-but-not-yet-hydrated boundary, React records the event, prioritises hydrating that boundary first, then replays the event. The senior implication: where you place Suspense boundaries is a performance design decision, not just an error/loading-state decision. A boundary around a slow, below-the-fold widget lets the rest of the page stream and hydrate without waiting for it; no boundary and that widget blocks the whole document.

Debug this
log
Warning: Text content did not match.
Server: "Good evening" Client: "Good morning"
  at Greeting (greeting.tsx:6)
  at Header
  at App

Warning: An error occurred during hydration. The server HTML
was replaced with client content in <div id="greeting">.

[CLS] layout shift: 0.14  (source: #greeting subtree)

The Greeting component renders 'Good morning' or 'Good evening' based on the hour. It mismatches on hydration and causes a 0.14 CLS shift. Why does it mismatch, and what is the correct fix?

Eliminate a hydration mismatch from a client-only value

1/3
Trace it
1/4

A server-rendered page shows a console warning 'Text content did not match. Server: 14:32 Client: 14:33' and a timestamp briefly flickers on load. What happened and how is it fixed?

1
Step 1 of 4
The component renders the current time directly — the server rendered it at one second, the client hydrated a second later, so the two renders disagree and React re-rendered the subtree
2
Locked
The server clock is wrong
3
Locked
The CDN cached a stale page
4
Locked
The JavaScript bundle is corrupted
Quiz

Why are RSC and SSR described as orthogonal rather than competing?

Recall before you leave
  1. 01
    What is a hydration mismatch and what are the three most common causes?
  2. 02
    Why must the server's API response data be embedded in the HTML for the first client render?
  3. 03
    How does placing a Suspense boundary around a slow widget affect streaming and hydration?
Recap

Hydration is a contract: React walks the server DOM and the client’s virtual tree in lockstep, adopting each node. When any node disagrees — because the component read Date.now(), Math.random(), window, or a locale-sensitive format during render — React discards the affected subtree and re-renders it on the client. The visible symptoms are a content flash and a CLS spike. The determinism rule: the first client render must be a pure function of the same inputs the server had. Client-only values move to useEffect so both environments produce identical DOM on the first pass. The same constraint applies to data: the server’s query snapshot must be embedded in the HTML (dehydrated cache) so the client’s first pass uses the same data without refetching. Streaming SSR with renderToPipeableStream sends the shell immediately and streams Suspense boundaries as their data resolves — boundary placement decides which content pays which latency, making it a performance design choice, not just a loading-state concern.

Connected lessons
appears again in143
Continue the climb ↑RSC, per-route strategy, and production observability
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.