The console warning reads: Text content does not match server-rendered HTML. Most fixes online are a copy-paste shrug. This guide walks through what the message actually means, the four causes that account for almost every real case, and the small set of fixes that resolve each.
What hydration is doing
When Next.js serves a page, the server runs your React components and emits HTML. The browser displays that HTML immediately — fast first paint, no waiting for JavaScript. Then React loads, runs the same components again in the browser, and walks the existing DOM attaching event listeners and restoring state. That second walk is hydration. The contract React relies on is that the server-rendered HTML and the first client render produce identical output. If they do not, React cannot match its component tree to the DOM and the page either re-renders from scratch (visible flicker) or breaks subtly.
The mismatch warning is the diagnostic React emits when it spots the first divergence. The location it reports is rarely the root cause — it is the first element where the trees differ, which is usually some distance below the input that actually diverges.
The four causes that account for almost everything
1. Reading window, document, or browser-only globals during render
On the server, window is undefined. Code that reads it tends to be defensively wrapped with typeof window !== "undefined", which produces a falsy branch on the server and a truthy branch in the browser. That is the mismatch — same component, different output, broken contract.
// Broken — diverges between server and client
function Greeting() {
const dark = typeof window !== "undefined"
&& window.matchMedia("(prefers-color-scheme: dark)").matches;
return <span>{dark ? "Hi there 🌙" : "Hi there ☀️"}</span>;
}Fix it by deferring the browser-only read into useEffect and starting with a server-safe initial value. The first paint shows the safe value; the effect runs after hydration and updates state on the client only.
function Greeting() {
const [dark, setDark] = useState(false); // safe initial value
useEffect(() => {
setDark(window.matchMedia("(prefers-color-scheme: dark)").matches);
}, []);
return <span>{dark ? "Hi there 🌙" : "Hi there ☀️"}</span>;
}2. Date and time values
The server renders at one instant in one timezone. The browser renders at a different instant, in the user's timezone. Any component that calls new Date(), Date.now(), or toLocaleString() during render will produce mismatched output. The fix is the same as above — compute the time on the client after hydration, with a neutral placeholder during the first paint.
function Timestamp({ iso }) {
const [local, setLocal] = useState("");
useEffect(() => {
setLocal(new Date(iso).toLocaleString());
}, [iso]);
return <time dateTime={iso}>{local || "—"}</time>;
}3. Random values used in render output
A component that calls Math.random() or generates a UUID inline will produce different values on each render. Server renders one set; client renders another. The fix is to generate the value once, outside the render path, and pass it in as a prop or pull it from a stable source (a database, a server action, the URL).
4. localStorage, sessionStorage, and other client-only state
The classic dark-mode-toggle bug. A component reads localStorage.getItem("theme") during render to decide which CSS class to apply. The server has no localStorage so it renders the default theme. The browser reads the saved value and renders the other theme. Mismatch every time.
The fix has two parts. First, gate the read on a mounted flag so the first client render still matches the server. Second, accept that there will be a one-frame flicker as the saved theme replaces the default — and either accept that flicker, or move the theme decision into a blocking inline script that runs before React loads (the pattern used by every major dark-mode implementation in production).
When suppressHydrationWarning is the right answer
The React team added suppressHydrationWarning as an escape hatch for one specific case: a single element whose content is expected to differ between server and client and which has no good way to be deferred. Timestamps are the canonical example. The escape hatch is intentionally narrow — it suppresses the warning on the element it's applied to and nothing else. Use it sparingly.
<time
dateTime={iso}
suppressHydrationWarning
>
{new Date(iso).toLocaleString()}
</time>A diagnostic order to follow when the cause is not obvious
- Open the React error overlay and identify the component the warning points at. The component is usually somewhere inside the actual cause, not the cause itself.
- Walk upward from that component, looking for the first ancestor that reads
window,document, a date, a random value, or anything in browser storage. - If nothing obvious shows up, comment out subtrees until the warning disappears. The last subtree you commented out contains the cause.
- Once located, fix using the patterns above. Resist the urge to reach for
suppressHydrationWarningas the first response.
Related reading
For the broader context on how React server and client rendering fit together, the glossary entry on hydration covers the boundary between Server Components and Client Components. If you're troubleshooting React errors more generally, the JavaScript debugging guide covers the disciplined moves that apply across every kind of frontend bug.
About the writers
Developer educator at ShareCode. Writes the tutorial track — Python, JavaScript debugging, coding-interview prep, and the everyday code-quality habits that hold up in real codebases.
More from Kajal
Founder of ShareCode. Writes the engineering deep-dives on this site — WebRTC, Firebase Auth, real-time sync, and the production patterns behind the editor itself.
More from Kishan
Hit a stubborn hydration bug?
Paste the failing component into a code space, share the link with a teammate, and walk through the diagnostic order together. A second pair of eyes catches the diverging input faster than another hour alone with the warning.