What Is a Hydration Error?#
When Next.js renders a page, it does it twice: once on the server (to send HTML to the browser), and once on the client (React "hydrates" that
HTML to make it interactive). If what React renders on the client doesn't match what the server sent, you get a hydration mismatch.
Error: Text content does not match server-rendered HTML. Warning: Expected server HTML to contain a matching <div> in <div>.
This usually shows as a white flash, broken layout, or the dreaded full-page re-render. In production it's silent but damages user experience and
Core Web Vitals.
The 5 Most Common Causes#
1. Date or Math.random() Called During Render#
Server renders at request time. Client renders at load time. Any value that changes between those two moments will mismatch.
function BlogDate() {
return <p>{new Date().toLocaleDateString()}</p>
}
▎ Fix: Move time-dependent values into useEffect so they only run on the client.
function BlogDate() {
const [date, setDate] = useState('')
useEffect(() => {
setDate(new Date().toLocaleDateString())
}, [])
return <p>{date}</p>
}
2. Browser-Only APIs in Render
window, localStorage, navigator — these don't exist on the server. If your component reads them during render, the server crashes or returns
undefined while the client gets the real value.
▎ Error: ReferenceError: window is not defined
// ❌ Crashes on server
function ThemeToggle() {
const theme = localStorage.getItem('theme') || 'light'
return <button>{theme}</button>
}
▎ Fix: Use typeof window !== 'undefined' guard or move to useEffect.
// ✅ Safe on both server and client
function ThemeToggle() {
const [theme, setTheme] = useState('light')
useEffect(() => {
setTheme(localStorage.getItem('theme') || 'light')
}, [])
return <button>{theme}</button>
}
3. Third-Party Scripts That Modify the DOM
Ad scripts, chat widgets, and analytics tools sometimes inject DOM nodes before React hydrates. React then finds unexpected elements and panics.
▎ Warning: Hydration failed because the server rendered HTML didn't match the client. The most common reason is that a browser extension modified
▎ the HTML before React could hydrate it.
▎ Fix: Use Next.js <Script> component with strategy="lazyOnload" instead of raw <script> tags in _document. This ensures third-party scripts fire
▎ after hydration completes.
import Script from 'next/script'
<Script src="https://cdn.example.com/widget.js" strategy="lazyOnload" />
4. Invalid HTML Nesting
React and the browser both enforce HTML nesting rules. If you nest a <div> inside a <p>, or a <p> inside an <a>, the browser silently fixes it
during parsing — but React's VDOM doesn't match the corrected DOM.
function Card({ text, children }) {
return (
<p>
{text}
<div>{children}</div>
</p>
)
}
▎ Fix: Use <div> or <span> consistently. Run your page through the W3C HTML validator if you're not sure.
5. Conditional Rendering Based on typeof window
This pattern looks safe but isn't — React tries to reconcile server output (where window is undefined) with client output (where it exists).
function Component() {
if (typeof window === 'undefined') return null
return <div>Client content</div>
}
▎ Fix: Use the mounted pattern instead.
function Component() {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
return <div>Client content</div>
}
Suppressing vs Fixing
You'll find this suggested online:
// ❌ Do not do this
<div suppressHydrationWarning>
{new Date().toLocaleDateString()}
</div>
suppressHydrationWarning hides the error but doesn't fix it. React still re-renders the whole subtree on the client, causing layout shift. Use it
only for values that genuinely cannot be known at SSR time (like browser fingerprints or ad content) and isolate it to a single element.
How to Debug Hydration Errors Fast
Next.js 13+ shows a detailed diff in development mode. Look for this output in your terminal or browser console:
Warning: Text content did not match.
Server: "Hello"
Client: "Hello!"
The server/client diff tells you exactly which component is mismatching.
▎ Note: In Next.js 14+, hydration errors include a component stack trace in dev mode. Run next dev (not next start) to see the full trace.
Then use DebugAI to paste the error alongside the component — it reads your full component tree and finds the mismatch source without you having
to binary-search through conditional renders.
Summary
┌──────────────────────────────────────┬─────────────────────────────────────────┐
│ Cause │ Fix │
├──────────────────────────────────────┼─────────────────────────────────────────┤
│ new Date() / Math.random() in render │ Move to useEffect │
├──────────────────────────────────────┼─────────────────────────────────────────┤
│ window / localStorage access │ Guard with useEffect or mounted pattern │
├──────────────────────────────────────┼─────────────────────────────────────────┤
│ Third-party DOM scripts │ Use <Script strategy="lazyOnload"> │
├──────────────────────────────────────┼─────────────────────────────────────────┤
│ Invalid HTML nesting │ Fix nesting, validate HTML │
├──────────────────────────────────────┼─────────────────────────────────────────┤
│ typeof window conditional render │ Use mounted state pattern │
└──────────────────────────────────────┴─────────────────────────────────────────┘
Hydration errors are always a server/client mismatch. Find what's different between the two environments and defer the client-only part to
useEffect.