Key Takeaway
A Next.js hydration error means the HTML rendered on the server did not match what React produced on the first client render. The fix is to make both renders identical, or to defer genuinely client-only values into useEffect so the initial output stays the same on both sides.
Few Next.js errors are as confusing on first sight as hydration failed because the initial UI does not match what was rendered on the server. The page often looks fine, yet the console lights up red and interactivity feels broken. I have hit this many times across App Router projects, and the cause is always the same shape of problem.
In this post I walk through what hydration actually is, why the mismatch happens, a repeatable way to hunt the offending line down, and the correct fixes for each cause. No guesswork, just the patterns the React and Next.js docs actually recommend.
What the hydration error really means
Next.js prerenders your page to HTML on the server. In the browser, React then hydrates that HTML, walking the same component tree and attaching event handlers to the existing DOM instead of recreating it. Hydration only works if the tree React builds on the client produces the same output as the server did.
When the first client render disagrees with the server HTML, React cannot trust the markup. It warns in development and falls back to client rendering for that subtree, which is slower and can attach handlers to the wrong elements. That is why the docs are blunt: treat mismatches as bugs and fix them, do not paper over them.
The common causes
Almost every hydration mismatch I have debugged traces back to one of a small set of causes. The React and Next.js docs list the same offenders, so once you recognise the shape you can usually name the culprit before opening the component.
- Invalid HTML nesting that the browser silently repairs, such as a div inside a p, or a p inside another p. The server emits your intended markup, the browser rewrites it, and the two trees no longer line up.
- Browser-only APIs used during render, like window, localStorage, or matchMedia. These do not exist on the server, so the server and client branches diverge, especially when guarded with a typeof window check.
- Time and locale dependent output during render, such as the Date constructor, toLocaleTimeString, or number and currency formatting that differs between the server timezone or locale and the visitor's.
- Random or non-deterministic values generated in render, like Math.random or a fresh unique id, which naturally produce a different result each time and therefore differ between server and client.
- Things outside your code entirely, most commonly browser extensions that inject or rewrite attributes and markup before React hydrates, plus mis-configured CDN minification or CSS-in-JS setup.
A systematic way to find it
The error text alone rarely points at the exact line, so I work through the same checklist every time instead of guessing:
- Read the diff React prints. Modern Next.js shows the mismatched text or attribute; note whether it is text content, an attribute, or a whole element that appears only on one side.
- Reproduce it clean. Open the page in a private window with extensions disabled. If the error disappears, an extension is rewriting your HTML and your code is fine.
- Bisect the tree. Comment out or short-circuit halves of the component until the warning stops, which isolates the offending subtree fast without reading every line.
- Ask what could differ between server and client for that node: a clock, a locale, a random value, a window check, or nested tags the browser would repair. That answer names the fix.
The correct fix: keep both renders identical
The primary fix is not to silence the warning, it is to make the server and the first client render produce the same output. For values that are genuinely client-only, render a stable placeholder on the server and fill in the real value after mount inside useEffect, which runs only in the browser.
// BEFORE — hydration mismatch.
// The server renders its own clock time; the browser renders a
// different time at hydration, so the initial UIs never match.
export default function LastUpdated() {
return <p>Last updated: {new Date().toLocaleTimeString()}</p>;
}
// AFTER — stable on the server, filled in on the client.
// The server and first client render both output an empty string
// (they match), then useEffect swaps in the real time after mount.
"use client";
import { useState, useEffect } from "react";
export default function LastUpdated() {
const [time, setTime] = useState("");
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <p>Last updated: {time}</p>;
}This is the pattern the Next.js docs recommend for time-dependent and browser-only values. The server and the first client render both output the same empty placeholder, so hydration succeeds, and the effect then updates the DOM a moment later without any mismatch.
Reach for next/dynamic with ssr false when a whole component can never render meaningfully on the server, such as a chart tied to window size. It disables prerendering for that component only, so nothing is emitted on the server to mismatch against.
When suppressHydrationWarning is the right tool
Sometimes a difference is genuinely unavoidable for a single element, the classic example being a timestamp. For that narrow case React offers suppressHydrationWarning. If a component must not render on the server at all, disabling SSR with a dynamic import is the cleaner option.
// Escape hatch: only for a single element whose value is
// genuinely unavoidable to match, such as a timestamp.
// It patches one level deep only — do not wrap whole subtrees.
<time dateTime="2026-08-01" suppressHydrationWarning>
{new Date().toLocaleDateString()}
</time>
// For content that must not render on the server at all,
// disable prerendering for that component instead:
import dynamic from "next/dynamic";
const ClientOnlyChart = dynamic(() => import("./chart"), {
ssr: false,
});Treat suppressHydrationWarning as an escape hatch, not a default. It works only one level deep, and when set React will not attempt to patch the mismatched text content, so anything below that element is on its own.
Do not wrap a whole subtree in suppressHydrationWarning to make the red console go away. It hides real bugs one level below, and because React stops patching the text, you can ship UI that silently differs between server and client without any warning.
Causes and fixes at a glance
Here is the mapping I keep in my head, matching each common cause to the fix the docs endorse:
| Cause | Why it mismatches | Correct fix |
|---|---|---|
| Invalid HTML nesting | Browser repairs the markup, so the DOM no longer matches the server tree | Fix the nesting so block elements are not placed inside a p or an a inside an a |
| Browser-only API in render | window or localStorage is undefined on the server, so the branches differ | Move the access into useEffect and render a stable placeholder first |
| Date, locale or number formatting | Server timezone or locale differs from the visitor's, so text differs | Format inside useEffect, or use suppressHydrationWarning on a lone timestamp |
| Random or unique values | Math.random and fresh ids produce a different value each render | Generate the value in useEffect, or use a stable id from useId |
| Browser extension or CDN | Third-party code rewrites the HTML before React hydrates it | Confirm in a clean profile; disable CDN auto-minify; it is not your code |
Wrapping up
Hydration errors feel mysterious until you internalise the one rule behind them: the server render and the first client render must produce identical output. Once that clicks, every fix follows from asking what could differ for a given node, and reaching for useEffect, a dynamic import, or a targeted suppressHydrationWarning in that order.

