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.
- Load the page and note the server value (first paint).
- Let hydration run (client attaches + renders).
- Observe the client value differs from server HTML.
- Result: mismatch → React patches/replaces nodes (flicker, resets, weirdness).
Buggy (render-time randomness)
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.
Detected issue
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: 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 HTMLCorrect
- 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: 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)
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
suppressHydrationWarningonly 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.