When hydration breaks determinism

Hydration is a contract: the client’s first render must exactly match the server HTML. Non-deterministic output breaks that contract.

Break it: non-deterministic render causes hydration mismatch

SSR paints HTML using a server snapshot. During hydration, the client renders again. If the value is non-deterministic, the client’s first render won’t match the server HTML.

Repro steps
  1. Load the page and note the server value (first paint).
  2. Let hydration run (client attaches + renders).
  3. Observe the client value differs from server HTML.
  4. Result: mismatch → React patches/replaces nodes (flicker, resets, weirdness).

Buggy (render-time randomness)

This demo intentionally breaks hydration.
Mismatch source: random
Server HTML value
0.545837
Client render value
0.621942
Status: MISMATCH
Mismatch
true
What usually happens

React detects server HTML text doesn’t match the client’s first render, warns in dev, and patches the DOM. In real apps this can produce flicker, lost state, and weird event behavior.

Note: Once hydration mismatches, React can only patch the DOM. The original contract cannot be restored without a full reload.

Signals of a hydration mismatch

  • Console warning about “Text content did not match”.
  • UI flickers (server value → client value).
  • Form fields reset / cursor jumps.
  • Events feel “dead” for a moment (hydration delays).

Why this breaks

Mental model: hydration requires a structural match
  • Hydration attaches behavior to existing SSR DOM; it does not rebuild the tree from scratch.
  • React expects the client’s first render output to be identical to the server HTML for the hydrated subtree.
  • Random/time/locale/env output during render produces different text/structure between server and client.
  • When the contract is broken, React must patch the DOM (or bail out), which can reset state and cause subtle bugs.
Broken invariant
Invariant: hydration requires deterministic output across server and client.

Detected issue

Detector
Mismatch = serverOutput !== clientFirstRender

Detection condition

const mismatch = serverText !== clientText;

Reference (correct mental model)

Correct mental model

Incorrect / Buggy

  • Render non-deterministic output (time/random/locale) during the first render.
  • Assume React will “just fix it” during hydration.
  • Let the hydrated subtree disagree with SSR HTML.

Buggy snippet

// ❌ Buggy: non-deterministic output during render // Server render: const serverValue = Math.random(); // Client render during hydration: const clientValue = Math.random(); // different return <div>{clientValue}</div>; // mismatch vs SSR HTML

Correct

  • Make the client’s first render deterministic and identical to SSR.
  • Pass server values as props for first paint.
  • Compute client-only values after mount (then update UI).

Correct snippet

// ✅ Correct: deterministic initial render // Server computes a deterministic value and sends it as a prop <Client valueFromServer={serverValue} /> function Client({ valueFromServer }) { // 1️⃣ Freeze the server value for the first client render const [value] = useState(valueFromServer); // 2️⃣ Client-only state (does NOT exist during hydration) const [clientOnly, setClientOnly] = useState(null); // 3️⃣ Compute client-only data AFTER mount useEffect(() => { setClientOnly(computeClientOnly()); }, []); return ( <> <div>{value}</div> {/* matches SSR HTML ✅ */} {clientOnly !== null && ( <div>{clientOnly}</div> {/* appears after mount ✅ */} )} </> ); }

Fix: guarantee determinism on the first render. + client-only updates after mount

The fix is user-visible: the fixed demo keeps the initial render identical to SSR (no mismatch), and moves client-only values to after-mount.

Fixed (deterministic first render)

Mismatch source: random
Server HTML value
0.545837
Client render value
0.545837
Client-only after mount
Status: OK
Mismatch
false
What changed

The first client render is identical to the server HTML (so hydration is stable). If you want client-only data (time/random/user context), compute it after mount.

Note: Hydration already completed. This only resets client-only state.

Safe patterns

  • Render server-provided props for the first paint.
  • Compute client-only values in useEffect.
  • For intentional differences: suppressHydrationWarning (sparingly).

Key takeaways

  • Hydration assumes byte-level equivalence between server HTML and the client’s first render.
  • Avoid time/random/user-agent/locale-dependent output during render.
  • Compute client-only values after mount (useEffect).
  • Mismatch can look like “random production bugs”: flicker, state resets, event weirdness.
  • Use suppressHydrationWarning only when divergence is explicitly acceptable.

Approach & tradeoffs

Approach

  • Identify which subtree is hydrated vs server-only.
  • Make first paint deterministic: pass server values as props.
  • Move client-only computations to after mount.
  • Optionally add a mismatch detector in dev.

Tradeoffs

  • You may show “stale” placeholders until mount (then update).
  • More wiring: server props / initial state.
  • Some UI must differ per client (A/B, personalization) → requires strict boundaries.
Common pitfall
“It’s fine, it’s just a small timestamp/random ID in the UI.” One mismatch can cause React to patch/replace DOM nodes and break user state. Keep the hydrated subtree deterministic on first render; if you need dynamic client data, render a placeholder and fill it after mount.