Hydration Explained: SSR, CSR, and Why It Breaks
A plain-English guide to hydration in React and Next.js: what CSR and SSR really do, why hydration errors happen, and how to fix the server-client mismatch.
- Next.js
- React
- Web Development
Your page loads fast, looks perfect, and then the console lights up red: "Text content does not match server-rendered HTML." Nothing on the screen looks broken. The layout is fine, the text is there. So what actually went wrong?
You hit a hydration mismatch. To understand it, you need to understand the two ways a page can get built and the awkward moment where they meet.
CSR vs SSR: who builds the HTML
There are two broad answers to "who renders this page?"
With client-side rendering (CSR), the server sends a nearly empty HTML shell plus a JavaScript bundle. The browser downloads the JS, runs it, and only then builds the actual page. The user may see little more than a shell until the JavaScript downloads, runs, and renders the page. It's flexible and easy to reason about, but the first paint is slow and it's rough for SEO, since a crawler may see an empty shell.
With server-side rendering (SSR), the server does the work upfront and sends real HTML. The browser paints something meaningful almost immediately, before any of your JavaScript has run. Faster first impression, friendlier to search engines.
Next.js App Router leans hard into the server side. Components are Server Components by default: they render on the server, can talk to your database directly, and ship less JavaScript to the browser. When you need interactivity (state, clicks, effects, browser APIs), you mark a component with "use client" and it becomes a Client Component. The server still renders it to HTML first, and the interactivity gets layered in afterward.
That "afterward" is where hydration lives.
Hydration: the handoff
SSR hands the browser HTML that looks finished but isn't alive yet. The button is there, styled and positioned, but clicking it does nothing. No state, no event handlers. Think of it as a photograph of your UI: accurate, but you can't interact with it.
Hydration is the step that brings the photo to life. React walks the server-rendered HTML already sitting in the DOM and attaches the event handlers and state that make it interactive, without rebuilding the markup from scratch.
That last part is the whole ballgame. React does not regenerate the DOM during hydration. It adopts the HTML that's already there and assumes it matches what it would have produced itself on this first render. Hydration is basically a promise: the server and the client will render the same thing.
Break that promise and you get the red error.
Why hydration breaks
A hydration mismatch means the HTML the server produced and the tree React rendered on its first client pass don't line up. React goes to attach its logic to nodes that aren't where it expected them, and it complains.
The causes are almost always the same handful:
- Rendering something that only exists in the browser:
window,localStorage, or anything behind atypeof windowcheck. The server has none of these, so it renders one thing and the client renders another. - Time and randomness.
new Date()orMath.random()in your render produces one value on the server and a different one a beat later in the browser. A clock that renders 10:00:00 on the server and hydrates at 10:00:01 is a mismatch by definition. - Invalid HTML nesting, like a
<div>inside a<p>, or an<a>inside another<a>. The browser silently "fixes" the structure, so the DOM no longer matches React's tree. - Outside interference: a browser extension that rewrites the HTML, a misconfigured CSS-in-JS setup, or a CDN that minifies the response in transit.
The common thread is simple: something differed between "rendered on the server" and "rendered in the browser for the first time."
How to fix it
The real fix is almost never a flag. It's making sure your server render and your first client render produce identical output.
If a component genuinely needs client-only data, render the neutral version first and update after mounting:
'use client'
import { useState, useEffect } from 'react'
export default function Clock() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
return <span>{isClient ? new Date().toLocaleTimeString() : 'Loading…'}</span>
}Because useEffect only runs on the client, after hydration, both the server and the initial client render show the same placeholder, then the real value swaps in once you're safely past the handoff. No mismatch.
For components that can't render on the server at all (a map widget that grabs window on mount, say), skip server rendering for that piece entirely:
import dynamic from 'next/dynamic'
const Map = dynamic(() => import('./map'), { ssr: false })And when a difference is truly unavoidable, like a timestamp that will never match down to the second, you can tell React to stop checking that one element with suppressHydrationWarning:
<time dateTime="2026-07-17" suppressHydrationWarning />Use it sparingly. It only silences one level of the tree, and it's an escape hatch, not a strategy. If you're sprinkling it everywhere, the mismatch is real and you're just hiding it.
Hydration debugging checklist
When I see a hydration error, I check:
- Does the render use
new Date(),Math.random(), or locale-specific formatting? - Does it read
window,localStorage, cookies, or browser-only APIs during render? - Is there invalid HTML nesting?
- Is a theme, auth state, or user preference different on server and client?
- Is a browser extension or CDN changing the HTML?
The fix is usually not complicated. The hard part is finding where the first render diverged.
The one rule worth remembering
Hydration is invisible when it works and loud when it doesn't. The whole thing comes down to one discipline: nothing you render should depend on information that differs between the server and the browser's first paint. Keep the two renders identical through that first pass, and let any differences appear afterward.
Do that and hydration fades into the background, which is exactly where you want it.
If you're staring down a stubborn hydration error and the usual fixes aren't sticking, get in touch and I'll help you track it down.