Topic 20.6
Hydration and Hydration Errors
In one line
Hydration is React rendering the same tree in the browser and adopting the server's HTML instead of creating new DOM; if the first client render produces different output (dates, random values, browser-only data, invalid nesting), React reports a hydration mismatch, so make the first client render match the server and change things after hydration.
Think of it like this
A paint-by-numbers kit. The server sends a printed outline (the HTML). In the browser, React gets the same numbered instructions (your components) and checks each region: "number 3 here, yes, it's there", and then adds the colour (event handlers and state). If the instructions say a cat but the printed outline shows a dog, React can't colour it in; it has to throw the page away and draw the cat itself. That's a hydration mismatch.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Hydration
- React adopting server-rendered HTML in the browser and attaching state and event handlers to it.
- hydrateRoot
- The
react-dom/clientfunction that starts hydration on a container that already has server HTML. - Hydration mismatch
- When the first browser render differs from the HTML the server sent.
- Recoverable error
- An error React can fix by re-rendering on the client; it's reported but the app keeps working.
- useId
- A hook that makes an id that is the same on the server and the client, for linking labels and inputs.
- suppressHydrationWarning
- An attribute that silences mismatch warnings for one element's own text and attributes.
- Two-pass rendering
- Rendering the server-safe version first, then the browser-specific version after hydration in an effect.
- Invalid nesting
- HTML elements placed where the HTML rules don't allow, like a
<div>inside a<p>.
Step by step
01Server renders, browser claims
The server sends HTML. The browser loads React and the same components, and React matches its first render to the existing nodes one by one.
02Spot the classic bug
This component renders the current time. The server renders at 10:00:00, the browser hydrates at 10:00:02: the text differs, so hydration fails.
function Clock() {
return <p>It is {new Date().toLocaleTimeString()}</p>; // differs every render
}03Fix it with two passes
Render something stable first (empty, or a server-provided time), then fill in the browser's value after hydration. Effects never run on the server.
function Clock() {
const [time, setTime] = useState(null); // same on server and first client render
useEffect(() => {
setTime(new Date().toLocaleTimeString());
const id = setInterval(() => setTime(new Date().toLocaleTimeString()), 1000);
return () => clearInterval(id);
}, []);
return <p>It is {time ?? "..."}</p>;
}04Or mark a known difference
When a small difference is expected and harmless, suppressHydrationWarning on that one element tells React not to complain. React keeps the server's text until the next update.
<time dateTime={post.iso} suppressHydrationWarning>
{new Date(post.iso).toLocaleString()}
</time>05Use useId for ids
Ids made with a counter or Math.random() differ between server and client. useId is derived from the component's place in the tree, so both sides agree.
function Field({ label }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}Try it yourself
- 1
Make the mismatch an attribute
In the first example, render
<p title={onServer ? "a" : "b"}>Same text</p>instead. Predict: error or warning? Is the title fixed afterwards? (Warning, and no.) - 2
Remove the fix
In the second example, change
useState(null)touseState(localStorage.getItem("name"))and run. The server can't read the browser's storage, so the first client render differs: you get the hydration error. - 3
Suppress one level only
Move
suppressHydrationWarningfrom<time>to the outer<div>. Does the warning for the<p>go away? (No: it only covers the element's own attributes and direct text.)
Code & diagrams
Simulated server (a flag instead of a real server), real React error. React prints the diff (`+` what the client rendered, `-` what the server sent) and re-renders on the client, so the screen ends up right but the server HTML was wasted.
Preview
Console
Nothing logged yet.Expected console output
Uncaught Error: Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<Where>
<p>
+ Rendered in the browser
- Rendered on the server
Screen: Rendered in the browserNo warnings this time: both components render the server's version during hydration and switch to the browser's value right after. `localStorage` here is the preview's in-memory copy.
Preview
Console
Nothing logged yet.Expected console output
server HTML: Welcome!Times shown in your time zone
after hydration: Welcome back, Asha | Times shown in UTC+5:30The `className` difference only warns, and the DOM keeps `server`: React will not fix attributes during hydration. The `<time>` difference is silenced by `suppressHydrationWarning`, and its server text also stays until the next update.
Preview
Console
Nothing logged yet.Expected console output
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<App>
<div>
<p
+ className="client"
- className="server"
>
+ attribute
...
p class: server
time text: server timeThe exact id format is an internal detail (it changed between React versions); treat ids as opaque strings. What matters is that both sides agree.
Preview
Console
Nothing logged yet.Expected console output
server ids: [ '_R_1_', '_R_2_' ]
client ids: [ '_R_1_', '_R_2_' ]Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Put a <div> inside a <p>
Render <p>Intro <div>block</div></p> in any component (no SSR needed).
Break #2
Generate ids with a counter
Write let next = 0; function Field() { const id = "field-" + next++; ... } and server-render it.
Myth vs fact
Myth
Hydration re-creates the page from scratch.
Fact
It reuses the server's DOM nodes and only attaches behaviour. It does run all your components again in the browser, which costs CPU time.
Myth
A hydration warning is harmless because the page still looks right.
Fact
Text mismatches throw away server HTML (slower, can flash). Attribute mismatches are never fixed, so the DOM can disagree with React state until the next update.
Myth
suppressHydrationWarning fixes the mismatch.
Fact
It only hides the message for one element. The server's value stays on screen until something re-renders that element.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Hydration errors are reported through
hydrateRoot'sonRecoverableErroroption (default:reportError, which is why the preview shows them asUncaught Error). Send them to your error tracker; in production React prints shorter messages with an error code. - ▸
Mismatch recovery is per Suspense boundary: the nearest boundary above the mismatch is client-rendered. Wrapping risky parts in their own
<Suspense>limits the damage. - ▸
React 19 tolerates unexpected tags inserted by extensions or third-party scripts in
<head>and<body>(it skips them instead of failing), but changes inside your components' DOM still cause mismatches. - ▸
useSyncExternalStorewithout agetServerSnapshotthrows during server rendering; with one, React uses it on the server and during hydration, then re-renders with the client snapshot, which is the cleanest way to read browser stores in SSR apps.
Remember this
- 1
How hydration works.
hydrateRoot(container, <App />)renders your components in the browser as if for the first time, but instead of creating DOM nodes it walks the existing server HTML in the same order and claims each node: this<button>belongs to that element, this text node to that string. Then it attaches event listeners, runs effects and the page becomes interactive. The DOM is reused, so there's no flash and inputs keep what was typed. For this to work, the first client render must produce exactly the same output as the server did. - 2
What happens on a mismatch. If the text or elements differ, React 19 throws away the server HTML up to the nearest Suspense boundary (or the whole root) and renders it again on the client: you see an error in the console (
Hydration failed because the server rendered text didn't match the client...) and lose the benefit of SSR there. If only attributes differ (aclassName, astyle), React logs a warning and does not fix them: the page keeps the server's attribute while React believes it set the client's value, a subtle bug. In both cases React 19 prints a diff (+client,-server) to help you find it. - 3
Common causes. (1) Time:
new Date().toLocaleTimeString()differs between the server render and the hydration moments later. (2) Randomness:Math.random()or a random id. (3) Locale and time zone: formatting a date or number on a server in UTC anden-USversus a browser inAsia/Kolkataandhi-IN. (4) Browser-only data:window.innerWidth,localStorage,navigator.language, or code liketypeof window !== "undefined" ? A : Binside render. (5) Invalid HTML nesting:<p><div/></p>or<a>inside<a>; the browser's HTML parser rearranges the server HTML, so the DOM no longer matches what React expects. (6) Browser extensions that change the page before React loads (password managers, translators), which you can't fully control. - 4
Fixes. For browser-only values, render the server version first and switch after hydration: keep the value in state, initialise it to what the server knows, and set the real one in
useEffect(which never runs on the server); or useuseSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)(Topic 9.6), which uses the server snapshot during hydration automatically. For time and locale, pass the value or the time zone from the server as data, or format dates in a fixed time zone. For random ids that connect labels and inputs use **useId()**, which produces the same id on server and client because it's based on the component's position in the tree. Fix invalid nesting in the markup (React warns about it during development even without SSR). - 5
**
suppressHydrationWarning.** AddingsuppressHydrationWarning={true}to an element silences the mismatch warning for that element's own text and attributes, one level deep. It's an escape hatch for unavoidable differences like a timestamp (<time suppressHydrationWarning>) or a theme class set by a script before React loads (<html suppressHydrationWarning>, which is how theme libraries avoid a flash). The server's value stays on screen until the next render; it doesn't make React patch it. Never use it to hide real bugs. - 6
Client-only rendering. Some components can't be server-rendered at all: a map library that touches
windowon import, a chart that measures the screen. Options: render a placeholder until mounted (the effect pattern), wrap them in<Suspense>and load them lazily on the client, or in Next.js usedynamic(() => import("./Map"), { ssr: false })fromnext/dynamicinside a client component. Also see Topic 20.1: React 18+ hydrates Suspense boundaries independently, so a mismatch inside one boundary only re-renders that boundary.
Explain it without notes
Explain step by step what hydrateRoot does.
List five causes of hydration mismatches and the fix for each.
What's the difference between a text mismatch and an attribute mismatch in React 19?
When is suppressHydrationWarning appropriate?
Practice
Rewrite function Width() { return <p>{window.innerWidth}px</p>; } so it works with SSR without a mismatch.
A server in UTC renders new Date("2024-01-15T10:30:00Z").toLocaleString(). Why can it mismatch, and how do you make it stable?
Trade-offs
- ↔
Two-pass rendering avoids mismatches but shows a placeholder briefly and causes an extra render; reserve space so the layout doesn't jump.
- ↔
Client-only components avoid SSR problems entirely but lose SSR's benefits for that part (no HTML for crawlers, later first paint).
Done when you can
I can explain how hydration reuses server HTML.
I can read React's hydration diff and find the cause.
I can fix mismatches from time, randomness, locale, browser APIs and invalid nesting.
I use
useIdfor ids that must match on both sides.I know when
suppressHydrationWarningis acceptable and what it doesn't do.I can render a component only on the client when I must.