Frontend · Next.js
Hydration: What It Is and Why It Breaks
The server sends HTML that already looks right — hydration is the step where React quietly takes it over, and the step where mismatched renders turn into console warnings.
Open a server-rendered Next.js page with JavaScript disabled and you'll still see fully formed HTML — the headline, the images, the article text, all there. That HTML didn't come from an empty <div id="root"> that got filled in later; it was generated on the server and sent down already "visible." Hydration is the step that happens next, in the browser: React walks that existing markup, rebuilds its internal component tree against it, and attaches the event listeners, state, and effects that make it interactive. It's not building the DOM from scratch — it's adopting DOM that already exists.
Why the attach step exists at all
It would be simpler, conceptually, if the browser just rendered everything from a blank slate. The reason it doesn't is that server-rendered HTML has a very real gap: it's visible, but it's inert. A button in that markup looks like a button and paints like a button, but until React finishes loading, parses the component tree, and wires up its onClick, clicking it does nothing at all. Hydration is the bridge across that gap — the point where a static page becomes an application. Skip it, and server rendering would only ever produce read-only documents; keep it, and you get fast first paint and eventual interactivity, at the cost of a step that has to run before the second half of that promise is kept.
What actually happens during hydration
React doesn't re-render the page and throw away the old HTML in favor of the new output. Instead, as it renders each component on the client, it compares what that render would produce against the DOM nodes already sitting there from the server. When they match, React just attaches its fiber to the existing node and moves on — no new elements created, no layout thrash. This is what makes hydration cheap compared to a full client-side render: most of the work of "building the UI" was already done by the server, and the client is mostly just labeling what's there.
Where mismatches come from
That comparison only works if the server and the client are rendering the same thing. A hydration mismatch happens whenever a component's output on the client's first render differs from what the server actually sent — and the classic causes all share one trait: they depend on something that isn't pinned to the component's props or state.
Date.now()ornew Date()rendered directly — the server evaluates it at request time, the client evaluates it milliseconds (or seconds) later, so the two timestamps almost never match.Math.random()used to generate an id or pick a value — by definition different on every call.- Branching on
typeof window !== "undefined"to conditionally render something — the server has nowindow, so it renders one branch; the client has one, so it renders the other.
When React detects this, it doesn't silently prefer one version. It logs a hydration warning (or, in stricter cases, throws), and then falls back to discarding the mismatched subtree and re-rendering it client-side — which is exactly the "flash" you sometimes see where content briefly changes right after a page loads. The warning is React telling you, precisely: "the HTML I was handed doesn't describe the same UI my first render just produced, and I don't know which one is right."
The error message is the symptom, not the bug
"Text content does not match server-rendered HTML" or "Hydration failed because the initial UI does not match" always points at the component, but the actual bug is almost always a value that isn't deterministic between the server pass and the client pass — a clock read, a random number, an environment check, or a locale/timezone difference.
A mismatch, and the fix
Here's a minimal version of the problem: a receipt component that stamps itself with the current time and a random id directly during render.
function OrderReceipt() {
// Rendered once on the server, then again on the client during hydration.
// The two runs happen at different instants, so they disagree.
const generatedAt = Date.now();
const receiptId = Math.random().toString(36).slice(2);
return (
<div>
<p>Receipt #{receiptId}</p>
<p>Generated at {generatedAt}</p>
</div>
);
}The server renders this once, at request time, and sends down HTML with one timestamp and one id baked in. The client then renders the same component during hydration — calling Date.now() and Math.random() again, producing different values. The two renders disagree, and React has no way to reconcile them.
The fix isn't to make the values "more consistent" — it's to make sure the server and the client render the exact same thing on the first pass, and defer the non-deterministic part until after hydration has already succeeded:
function OrderReceipt() {
const [meta, setMeta] = useState<{ generatedAt: number; receiptId: string } | null>(
null
);
// Runs only in the browser, after the server HTML has already
// been painted and hydration has completed — so there's nothing
// for the client's first render to disagree with.
useEffect(() => {
setMeta({
generatedAt: Date.now(),
receiptId: Math.random().toString(36).slice(2),
});
}, []);
if (!meta) {
// Server and client both render this exact placeholder first.
return <div>Loading receipt…</div>;
}
return (
<div>
<p>Receipt #{meta.receiptId}</p>
<p>Generated at {meta.generatedAt}</p>
</div>
);
}Both the server and the client now render the loading placeholder as their first output — nothing to disagree about. Only after hydration completes does useEffect run, compute the timestamp and id in the browser, and update the state, which triggers a perfectly ordinary client-side re-render. Nothing about that second render needs to match server HTML, because hydration is already done.
The cost hydration imposes even when it works
Getting hydration to succeed isn't the end of the story — it also isn't free. To hydrate a component, the browser has to download its JavaScript, parse it, execute it, and run React's reconciliation against the existing DOM. That cost scales with how much of the page is interactive, not with how much of the page is visible. A content-heavy page with a handful of interactive widgets still ships and hydrates the framework runtime plus every component on the page, even though the user could already read everything the moment the HTML arrived. The HTML was never the bottleneck; the JavaScript needed to make it clickable is.
Why the industry keeps chipping away at it
This is precisely the cost that React Server Components and selective/partial hydration are aimed at. Server Components let whole subtrees render on the server and ship as HTML with no client-side component code at all — no bundle, no hydration pass, because there's nothing on the client that needs to reconcile against anything. More broadly, an "islands" approach — rendering a page as mostly static HTML with a few independently hydrated interactive components embedded in it — is the general pattern several frameworks have converged on to avoid hydrating a page that's 95% static just because 5% of it has a button. The specifics differ by framework, but the underlying idea is the same one this article started with: hydration isn't inherently expensive, doing more of it than the page actually needs is.
A quick mental check
If a value can differ between "evaluated on the server, right now" and "evaluated in the browser, a moment later," it doesn't belong in the render path that has to match. Move it into an effect, or seed it from a value that's already fixed by the time the server renders — like a prop or a URL param.
Key takeaways
- Hydration is React attaching to server-rendered HTML — reconciling it and wiring up listeners — not rebuilding the DOM from scratch.
- Without it, server-rendered HTML would stay visible but inert until the client JS finished loading and took over.
- Mismatches come from non-deterministic render output — Date.now(), Math.random(), typeof window checks — that differs between the server pass and the client's first render.
- The fix is to render an identical, deterministic first pass on both sides and defer the differing value to useEffect, after hydration has already succeeded.
- Hydration's cost scales with how much of the page is interactive, not how much is visible — which is exactly what Server Components and islands-style partial hydration are designed to reduce.
