Errors and fixes
Hydration failed because the initial UI does not match: causes and fixes
React 18 and 19 compare the server HTML with the first client render. When they differ, React rebuilds the tree: real causes and how to isolate them.
Key points
- The message means a non-deterministic render: dates, random numbers, localStorage or invalid HTML.
- The fix is to move the variable value after mount or render it on the server, not to silence the warning.
- `suppressHydrationWarning` covers the text of one element only and does not repair the cause.
Short answer: the first render on the client produced a different tree from the one the server wrote into the HTML. React throws the server HTML away and renders again: the page appears, but you lose the SSR benefit and see a flash of content.
The exact message
Hydration failed because the initial UI does not match what was
rendered on the server. There was an error while hydrating. Because
the error happened outside of a Suspense boundary, the entire root
will switch to client rendering.
The second sentence matters most: outside of a Suspense boundary. If the diverging component sits inside a Suspense boundary, React recovers only that branch. Otherwise it rebuilds the whole root.
The four real causes
| Cause | Typical symptom | Fix |
|---|---|---|
| Non-deterministic value in the render | dates, counters, random numbers | move the calculation into an effect |
| Invalid HTML | <div> inside <p>, <div> inside <tbody> | fix the nesting |
| Browser data read during the render | localStorage, matchMedia, saved theme | read it after mount, both sides start equal |
| Browser extension | attributes added before hydration | check in an incognito window |
The fix, in order
Before you touch the code, reproduce the problem in an incognito window without extensions. If it disappears, your application is not the culprit.
If it stays, look for a non-deterministic render. The most common case is a date: the server runs in UTC, the browser uses the user's time zone, and new Date().toLocaleString() returns two different strings.
// sbagliato: il server e il client formattano in fusi diversi
function LastUpdate() {
return {new Date().toLocaleString()};
}
// corretto: il testo arriva già formattato dal server
function LastUpdate({ label }) {
return {label};
}
When the value exists only in the browser, start from a neutral value and update it after mount. The intermediate state is identical on both sides, so hydration matches.
"use client";
import { useEffect, useState } from "react";
export function LocalTime() {
const [now, setNow] = useState(null);
useEffect(() => setNow(new Date().toLocaleTimeString()), []);
return {now ?? "—"};
}
For generated identifiers React offers useId(), which returns the same value on the server and the client for the same tree.
When the fix does not work
suppressHydrationWarning silences the warning for the text of a single element. It does not fix the cause. Put it on a large container and React stops reporting the problem while still regenerating the tree: you gain silence, not speed. The other exception is HTML produced by an external library you cannot change. Isolate that component behind a client-side mount.
How to prevent it
- One rule: the first render must depend on props alone, never on the clock, the browser or the environment.
- Validate the HTML: forbidden nesting produces a mismatch even when the markup looks correct.
- In CI, open the main pages with JavaScript disabled: what you see is the server render.
Where to go next
In the catalogue you find React components already written for hydration and hero blocks with a preview; the icons live in /icons. Go deeper with the App Router structure and dark mode without a second design system; the comparison of primitives is in Radix, Base UI and React Aria.
Frequently asked questions
The page still works, so should I fix the error?
Yes. React throws the server tree away and renders again on the client, so you lose the SSR benefit and pay for a flash.
Why does it only happen in production?
In development the server and the client share the same time zone and the same machine. In production the server runs in UTC and the browser does not, so formatted dates and toLocaleString diverge.
Can a browser extension cause it?
Yes. Extensions that change the DOM before hydration produce the same message. Test in an incognito window with the extensions off.
Is suppressHydrationWarning a fix?
It is a patch for a single text node, useful only for content that legitimately changes, such as a timestamp. It fixes neither invalid nesting nor random values.
How do I find the component at fault?
React 19 prints a stack with the component in it. If the stack is generic, disable half the page blocks: the difference between two renders points at the branch.