What is hydration in SSR, why do 'hydration mismatch' errors happen, and how do you fix them?
Understanding the two-pass render contract of SSR and the deterministic-first-render rule that keeps it intact.
In server-side rendering the server runs your components once and emits inert HTML — real markup the browser paints immediately (fast first paint, SEO-friendly), but with no event handlers or state attached. Hydration is the client's second pass: React runs the SAME components again in the browser and, instead of recreating the DOM, walks the existing server HTML and attaches handlers and state to it (via hydrateRoot). The contract is strict: the client's FIRST render must produce markup identical to what the server sent. A 'hydration mismatch' is React discovering the trees disagree. Common causes are all forms of non-determinism between the two environments: Date.now()/new Date()/Math.random() (different values each run), locale/timezone/number formatting that differs server vs client, reading browser-only globals during render (window, localStorage, navigator, matchMedia — undefined on the server so the branch differs), and invalid HTML nesting the browser 'fixes' (a <div> inside a <p>) so the DOM no longer matches. The fixes follow from 'make the first client render match the server': (1) the two-pass pattern — render the deterministic/server version first, then read browser-only values in useEffect and set state, causing a SECOND render that safely diverges post-hydration; (2) gate browser-only UI behind a mounted flag (useState(false) → true in an effect) so it renders nothing until after hydration; (3) use useId() for ids that must be stable and matching across server and client instead of a random/counter id; (4) suppressHydrationWarning as a targeted last resort for genuinely unavoidable diffs like a timestamp. The anti-pattern is branching on window during render — always defer that to an effect.
Diagnosing hydration-mismatch warnings, rendering browser-only or time/locale-dependent UI under SSR, and generating SSR-safe ids.
// BAD: reads window during render — server has no window, so the
// first client render diverges from server HTML => hydration mismatch.
function BadWidth() {
return <span>{window.innerWidth}px</span>;
}
// GOOD: deterministic first render (matches server), then a second
// render after mount reads the browser value safely.
function GoodWidth() {
const [width, setWidth] = React.useState(null); // same on server & first client render
React.useEffect(() => setWidth(window.innerWidth), []);
return <span>{width == null ? '…' : width + 'px'}</span>;
}
// SSR-safe id: stable and identical across server and client.
function Field() {
const id = React.useId();
return (<><label htmlFor={id}>Email</label><input id={id} /></>);
}