React Hydration Mismatch — Server and Client HTML Differ
Diagnose hydration errors by finding non-deterministic render output, browser-only state, invalid HTML nesting, or locale differences between server and client.
Symptoms
- Console reports 'Hydration failed because the server rendered HTML didn't match the client'
- The page flashes or re-renders immediately after load
- A timestamp, random value, or theme differs between view-source and the rendered DOM
Possible causes
- Rendering Date.now(), Math.random(), or a new object identity during the initial render
- Reading window, localStorage, or matchMedia before hydration completes
- Invalid HTML nesting that the browser repairs differently from React's output
- Locale, timezone, or currency formatting differs between server and browser
Diagnosis steps
Compare server HTML with the first client render
curl -s https://example.com/page | grep -A2 -B2 'timestamp\|theme'Interpretation
View-source shows the server output. If the first client render uses a different value, hydration cannot match.
Search the component tree for non-deterministic values
grep -rn 'Math.random\|Date.now\|new Date()\|localStorage\|window\.' src/componentsInterpretation
Any of these executed during render can produce different markup on the server and client.
Inspect the hydration error stack and surrounding HTML
DevTools Console → expand the hydration warningInterpretation
React usually identifies the mismatched element. Check its parent for invalid nesting such as div inside p or button inside button.
Root cause
React reuses the server-rendered DOM during hydration. If the first client render produces different markup, React cannot reliably attach event handlers and reports a mismatch. The difference comes from non-deterministic values, browser-only APIs during render, invalid HTML, or environment-dependent formatting.
Fix
Move browser-only values into useEffect or initialize them after mount. Use stable defaults for the first render, format dates with an explicit locale and timezone, and fix invalid HTML nesting. Use suppressHydrationWarning only for deliberate, isolated differences.
Risk note
suppressHydrationWarning hides the symptom and can leave event handlers attached to the wrong elements. Do not use it to silence a real mismatch.
Verify the fix
Hard reload with the cache disabled and confirm the hydration warning is gone. Compare the server HTML with the client DOM, run with JavaScript disabled, and add a test that renders the component on the server and client with the same props.
Hydration expects equality
The server sends HTML, and React attaches behavior to that HTML on the client. If the first client render expects different nodes, React cannot reconcile them cleanly. The components look fine until interactivity breaks or the tree re-renders.
// ❌ Different on server and client
function Clock() {
return <span>{Date.now()}</span>;
}
// ✅ Stable first render, update after mount
function Clock() {
const [now, setNow] = useState<number | null>(null);
useEffect(() => setNow(Date.now()), []);
return <span>{now ?? '—'}</span>;
}
Diagnose
Use view-source or curl to see the server HTML. Disable JavaScript to confirm what the server rendered. Search for window, localStorage, Math.random, Date.now, and locale-dependent formatting in render paths. Check for invalid nesting; browsers repair it silently, which changes the DOM before React hydrates.
Verify
- Hard reload shows no hydration warning.
- The first client render matches the server markup.
- Interactive controls work immediately after load.
- Server and client use the same locale and timezone, or the value is deferred until mount.
Avoid blanket suppression
suppressHydrationWarning is appropriate for a single intentionally variable text node, such as a timestamp with a documented fallback. Applying it broadly hides real differences and makes future debugging harder.