August 27, 2026
Hydration Mismatch: When a Server and a Browser Disagree
Frameworks that render HTML on a server and then take over in the browser depend on both sides producing identical output. Here's what happens when they don't, and why it's a named, well-understood bug category rather than a mystery.
Frameworks that render HTML on a server and then attach interactivity to it in the browser (React with server-side rendering, Next.js, and similar tools) depend on a strict guarantee: the HTML the server produced and the HTML the client would produce from the same component tree must be identical. The client-side pass doesn't rebuild the page from scratch; it walks the already-rendered DOM and attaches event handlers and internal state to what's already there, on the assumption that what's there matches what it would have rendered itself. When that assumption is false, the result is a hydration mismatch, React's own official term for this, used directly in its documentation.
Why it's worth taking seriously
React's own docs are blunt about the consequences, and the range is wider than it might seem for what sounds like a cosmetic inconsistency. In the best case, a mismatch causes a visible flicker as the client corrects the server's output after the fact. In the worst case, "event handlers can get attached to the wrong elements" entirely, because React's internal bookkeeping about what the DOM looks like no longer matches reality, and the mismatch can cascade into behavior that has nothing obviously to do with the original inconsistency.
This makes hydration mismatches a genuinely different category of bug from an ordinary logic error: the code causing it can be entirely correct in isolation, and the bug only exists because of a difference between two separate execution environments (server, client) running the same function and expecting the same result. It's relevant to any framework built on this render-on-the-server, attach-in-the-browser model, not one specific tool's quirk, but a structural consequence of the architecture itself.
What actually causes it
The root cause is always the same shape: a render function that's supposed to be pure and deterministic produces different output depending on where it runs. React's documentation lists several concrete, common triggers, all instances of the same underlying problem:
typeof window !== "undefined"branches, sincewindowexists on the client but not the server- Browser-only APIs like
window.matchMediacalled during the render itself Date.now(),Math.random(), or anything else that legitimately differs between two separate executions- Locale- or timezone-dependent formatting (
new Date().toLocaleDateString()) where the server's locale differs from the visitor's - Data that's fetched differently, or that has changed, between the server render and the client render
The standard fix is a two-pass render: return a server-safe placeholder on the first render, then
correct it after the component has mounted on the client, via useState initialized lazily and
updated in an effect. For genuinely unavoidable cases, React exposes suppressHydrationWarning as
an explicit escape hatch, but the documentation is direct about its scope: "This only works one
level deep, and is intended to be an escape hatch. Don't overuse it." It silences the warning for
one element's mismatched attribute, not for a whole subtree.
function RandomBadge() {
// Wrong: Math.random() differs between server and client render passes.
// return <span>{Math.random()}</span>;
// Right: render nothing meaningful until mounted, then compute client-side.
const [value, setValue] = useState(null);
useEffect(() => setValue(Math.random()), []);
return <span>{value ?? "n/a"}</span>;
}Next.js has a dedicated documentation page for the exact error message this produces ("Text content does not match server-rendered HTML"), which is itself a signal of how common the category is in practice: common enough to warrant its own permanent, indexed page rather than being treated as an edge case.
Applying it
- Treat any value read at render time that could plausibly differ between two separate executions (time, randomness, browser-only globals, locale) as a hydration risk, and defer it to a post-mount effect rather than computing it inline during render.
- When a hydration warning appears, resist the instinct to reach for
suppressHydrationWarningfirst. It hides the symptom for one element without addressing whatever non-deterministic value caused it, and the same root cause can resurface elsewhere. - Reserve
suppressHydrationWarningfor cases that are genuinely, unavoidably different by design (a component that legitimately needs to show different content once it knows it's running in the browser), rather than as a first response to an unexplained warning. - If a mismatch is hard to reproduce locally, check for anything environment-dependent first: timezone, locale, or a data source that could plausibly have changed between the server response and the client's own fetch of the same data.