Next.js Hydration Errors Explained With Real Fixes, Not Hand-Wavy Advice
A direct guide to diagnosing and fixing Next.js hydration mismatches, including browser-only APIs, time-dependent rendering, invalid HTML structure, and client-only components.
Why this error is so annoying: hydration failures make frontend teams doubt everything at once. Is the problem React? Is it SSR? Is it a race condition? Sometimes the error message is vague, but the root causes are usually very ordinary.
What hydration mismatch actually means
When Next.js server-renders a page, the HTML sent from the server has to match what React expects when the client takes over in the browser. If the server output and client output differ at the moment React hydrates, you get the classic mismatch errors.
The official Next.js docs point to several common causes, and they are worth taking seriously because most real bugs fall into this list:
- invalid HTML nesting
- browser-only APIs used during render
- time-dependent values like
Date() - random values like
Math.random() - client/server branches that produce different markup
Case 1: browser-only APIs inside render
This is one of the fastest ways to break hydration.
Bad:
export default function ThemeLabel() {
const theme = localStorage.getItem("theme");
return <span>{theme}</span>;
}Why it fails:
- the server cannot read
localStorage - the client can
- server markup and client markup diverge
Fix it by moving browser-only logic into an effect:
"use client";
import { useEffect, useState } from "react";
export default function ThemeLabel() {
const [theme, setTheme] = useState("light");
useEffect(() => {
const saved = window.localStorage.getItem("theme");
if (saved) setTheme(saved);
}, []);
return <span>{theme}</span>;
}Case 2: time-dependent rendering
This pattern quietly causes mismatches:
export default function Timestamp() {
return <div>{new Date().toISOString()}</div>;
}The server time and client time are not guaranteed to match.
Safer pattern:
"use client";
import { useEffect, useState } from "react";
export default function Timestamp() {
const [time, setTime] = useState("");
useEffect(() => {
setTime(new Date().toISOString());
}, []);
return <div>{time || "Loading..."}</div>;
}If you deliberately want client and server to differ for a specific text fragment, Next.js also documents suppressHydrationWarning, but that should be a narrow escape hatch, not your default design.
Case 3: invalid HTML structure
This one feels embarrassing because the app may “look fine” while still breaking hydration.
Bad:
export default function BrokenMarkup() {
return (
<p>
Intro text
<div>Nested block</div>
</p>
);
}That is invalid structure. Browsers repair invalid HTML differently, which means the DOM React hydrates may not match the one it expected.
Correct it:
export default function ValidMarkup() {
return (
<div>
<p>Intro text</p>
<div>Nested block</div>
</div>
);
}Case 4: client-only libraries that should not SSR
Charts, editor widgets, maps, and some UI libraries are often better loaded client-side only.
Use dynamic import with SSR disabled:
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
ssr: false,
});
export default function Dashboard() {
return <HeavyChart />;
}This is better than pretending the library is SSR-safe when it is not.
A debugging workflow that works fast
When you hit hydration issues, do not change five files at once. Use a narrower checklist:
- Find the smallest component mentioned in the stack trace
- Look for
window,document,localStorage,sessionStorage - Look for
Date(),Math.random(), locale formatting, or unstable IDs - Look for invalid nesting
- Check whether a third-party component should be client-only
The bigger architectural lesson
A lot of hydration bugs are really rendering-discipline bugs. The server render path should stay predictable. The client enhancement path can be dynamic, but only after hydration. Teams that mix those two phases carelessly tend to keep rediscovering the same bug under different names.
If you want fewer hydration errors, the rule is simple: render stable markup first, then layer on browser-only behavior intentionally.