Few errors in the React ecosystem induce as much frustration as the dreaded hydration mismatch:
Error: Hydration failed because the initial UI does not match what was rendered on the server.Warning: Expected server HTML to contain a matching <div> in <p>.
This error signals a fundamental disagreement between the HTML Next.js generated on the server and the Virtual DOM React attempted to construct on the client. It forces React to discard the server-rendered markup and regenerate the UI from scratch, causing layout shifts and effectively negating the performance benefits of SSR (Server-Side Rendering).
The Root Cause: How Hydration Works
To understand the fix, you must understand the mechanism.
- Server Phase: Next.js renders your component tree into an HTML string. This snapshot implies a specific state at time $T=0$.
- Transport: The HTML is sent to the browser and painted immediately (First Contentful Paint).
- Client Phase (Hydration): React loads its JavaScript bundle. It traverses the existing DOM nodes and attempts to attach event listeners to them.
The Crash: During step 3, React expects the DOM structure to match its Virtual DOM exactly. If the browser tampered with the HTML (due to invalid syntax) or if the logic produced different results on the client (due to window or localStorage), React flags a mismatch.
Scenario 1: The Invalid HTML Trap
The most common cause of this error is technically valid JSX that generates invalid HTML. Browsers are forgiving; if they encounter syntactically invalid HTML, they auto-correct it. React's hydration logic, however, is strict.
The Problem
The HTML specification dictates that a <p> tag cannot contain block-level elements like <div>, <section>, or headers (<h1>, <h2>).
If you render this:
// ❌ BAD
<p>
<div className="alert">Error occurred</div>
</p>
The browser parses the HTML and "fixes" it by closing the <p> tag immediately before the <div>:
<p></p>
<div class="alert">Error occurred</div>
<p></p>
When React hydrates, it looks for a child div inside the p. It finds an empty p instead. Mismatch.
The Solution
Use semantic HTML correctly. If you need a wrapper, use a <div>, <section>, or <span> depending on context.
// ✅ GOOD: Use a span for inline elements inside a paragraph
<p>
<span className="alert">Error occurred</span>
</p>
// ✅ GOOD: Use a div wrapper if the child is block-level
<div className="wrapper">
<div className="alert">Error occurred</div>
</div>
Scenario 2: Browser-Only APIs (window, localStorage)
The second most common cause is rendering data that exists only on the client.
The Problem
If you access window, localStorage, or document directly in your component body or specifically in the initialization of a useState hook, the server (Node.js runtime) and client will diverge.
// ❌ BAD: Direct access to localStorage causing mismatch
import { useState } from 'react';
export default function ThemeToggler() {
// On Server: localStorage is undefined (crash) or mocked to null.
// On Client: localStorage returns 'dark'.
// Result: Hydration mismatch.
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');
return <div className={theme}>Current Theme: {theme}</div>;
}
The Solution: The useMounted Hook pattern
To fix this, we must ensure the initial render matches the server (usually a default state) and update the state only after the component has mounted on the client.
While you can write a useEffect in every component, the cleanest approach is a custom hook.
1. Create useIsClient.ts
import { useState, useEffect } from 'react';
export function useIsClient() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return isClient;
}
2. Implement Safe Rendering
// ✅ GOOD: Defer client-specific rendering
import { useState, useEffect } from 'react';
import { useIsClient } from './hooks/useIsClient';
export default function ThemeToggler() {
const isClient = useIsClient();
const [theme, setTheme] = useState('light');
useEffect(() => {
// This only runs on the client, after the initial render pass
const storedTheme = localStorage.getItem('theme');
if (storedTheme) {
setTheme(storedTheme);
}
}, []);
// Option A: Render nothing until hydration is complete (prevents flash of wrong theme)
if (!isClient) return null;
// Option B: Render a loading state or default server-safe skeleton
return (
<div className={theme}>
Current Theme: {theme}
</div>
);
}
Scenario 3: Timestamps and Randomness
Rendering new Date() or Math.random() directly in the JSX will always fail because the execution time on the server differs from the execution time on the client.
The Solution: suppressHydrationWarning
For simple text content mismatches (like timestamps) where the difference is acceptable and expected, React provides an escape hatch.
export default function Timestamp() {
const time = new Date().toLocaleTimeString();
return (
<span suppressHydrationWarning>
Last updated: {time}
</span>
);
}
Note: Only use suppressHydrationWarning for text content. Do not use it to mask structural HTML mismatches or logic errors, as it only suppresses the warning one level deep.
Summary
Resolving hydration errors is not just about silencing a console warning; it is about ensuring the integrity of your application's rendering pipeline.
- Validate your nesting: Never put a
<div>inside a<p>. - Enforce Determinism: The initial render returned by your component must be identical on Node.js and the Browser.
- Two-Pass Rendering: If you need browser data (Window/LocalStorage), render a default state first, then update state inside
useEffect.