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.

frontendreactweb-development

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, since window exists on the client but not the server
  • Browser-only APIs like window.matchMedia called 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 suppressHydrationWarning first. It hides the symptom for one element without addressing whatever non-deterministic value caused it, and the same root cause can resurface elsewhere.
  • Reserve suppressHydrationWarning for 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.