Fix: React Hydration Error — Text Content Does Not Match
Quick Answer
How to fix React hydration errors — server/client HTML mismatches, useEffect for client-only code, suppressHydrationWarning, dynamic content, and Next.js specific hydration issues.
The Error
React throws a hydration mismatch error in the browser console:
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 <div>.Or in Next.js:
Error: Text content does not match server-rendered HTML.
See more info here: https://nextjs.org/docs/messages/react-hydration-errorOr a more specific mismatch:
Warning: Prop `className` did not match. Server: "dark" Client: "light"
Warning: Expected server HTML to contain a matching <p> in <body>.The page renders but the content flickers, or interactive elements don’t work correctly after the initial load.
Why This Happens
Hydration is the process where React attaches event listeners to server-rendered HTML. React expects the client-side render output to exactly match the server-rendered HTML. If they differ, React throws a hydration error and falls back to a full client-side re-render.
Common causes of mismatches:
- Browser-only APIs in render —
window,localStorage,navigator,documentdon’t exist on the server. Code that reads them returns different values server vs. client. - Random or time-dependent values —
Math.random(),Date.now(),new Date()produce different values on server and client. - Browser extensions — ad blockers, password managers, and translation extensions modify the DOM after server render but before hydration.
- Invalid HTML nesting —
<p>inside<p>,<div>inside<p>— browsers auto-correct invalid HTML differently than React expects. - Conditional rendering based on client state — checking
localStorage, cookies, or auth state that differs from the server’s assumption. - CSS-in-JS class names — some CSS-in-JS libraries generate non-deterministic class names.
- Third-party scripts modifying the DOM before React hydrates.
Fix 1: Move Browser-Only Code to useEffect
Code that accesses browser APIs must run only on the client. useEffect runs only after the component mounts — never on the server:
// WRONG — window is undefined on server
function ThemeToggle() {
const [theme, setTheme] = useState(
window.localStorage.getItem('theme') || 'light' // ReferenceError on server
);
return <button>{theme}</button>;
}
// WRONG — initial value differs server/client
function ThemeToggle() {
const [theme, setTheme] = useState(
localStorage.getItem('theme') || 'light' // 'light' on server, stored value on client
);
// Hydration mismatch: server renders "light", client renders "dark"
return <button>{theme}</button>;
}// CORRECT — read browser APIs only after mount
function ThemeToggle() {
const [theme, setTheme] = useState('light'); // Server-safe default
useEffect(() => {
// Runs only on client, after hydration completes
const stored = localStorage.getItem('theme');
if (stored) setTheme(stored);
}, []);
return <button>{theme}</button>;
}Pattern — track whether component is mounted:
function ClientOnly({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null; // Server renders nothing — no mismatch
return <>{children}</>;
}
// Use for browser-only components
<ClientOnly>
<ThemeToggle />
<BrowserStorageDisplay />
</ClientOnly>Fix 2: Use dynamic() with ssr: false in Next.js
Next.js provides a built-in way to disable server-side rendering for specific components:
// next.js — disable SSR for a component
import dynamic from 'next/dynamic';
const ThemeToggle = dynamic(
() => import('./ThemeToggle'),
{ ssr: false } // Component only renders on client — no hydration mismatch
);
const Map = dynamic(
() => import('./Map'),
{
ssr: false,
loading: () => <div>Loading map...</div>, // Shown server-side and during client load
}
);
export default function Page() {
return (
<div>
<ThemeToggle /> {/* Client-only — no SSR */}
<Map /> {/* Client-only — no SSR */}
</div>
);
}ssr: false trade-off — the component doesn’t render on the server at all, so search engines can’t index its content. Use it only for truly browser-only components (maps, charts, browser storage readers).
Fix 3: Use suppressHydrationWarning for Intentional Differences
Some differences between server and client are intentional and harmless — formatted dates, user-specific greetings. Suppress the warning on specific elements:
// Suppress warning on a specific element
function Timestamp({ date }: { date: Date }) {
return (
<time
dateTime={date.toISOString()}
suppressHydrationWarning // ← Suppresses mismatch warning for this element only
>
{date.toLocaleString()} {/* Format differs server/client based on locale */}
</time>
);
}
// Only suppresses the warning on the element it's applied to — not its children
<div suppressHydrationWarning>
{new Date().toLocaleTimeString()}
</div>Warning:
suppressHydrationWarninghides the warning but doesn’t fix the mismatch. React still re-renders the element on the client. Use it only when the difference is cosmetic (time formatting, locale) and not functional. Don’t use it to silence real bugs.
Fix 4: Fix HTML Nesting Errors
Browsers silently fix invalid HTML nesting, producing a different DOM than React expects:
// WRONG — <p> cannot contain block-level elements
// Browser removes the inner <div>, causing a mismatch
<p>
Some text
<div>Block element</div> {/* Invalid — <div> inside <p> */}
</p>
// WRONG — <ul> can only contain <li>
<ul>
<div>Not a list item</div> {/* Invalid */}
</ul>
// CORRECT
<div>
<p>Some text</p>
<div>Block element</div>
</div>Common invalid nesting patterns in React:
// WRONG — interactive elements can't be nested
<button>
<a href="/link">Click me</a> {/* <a> inside <button> — invalid */}
</button>
// CORRECT — use one or the other
<a href="/link" role="button" className="btn">Click me</a>
// WRONG — <table> elements must follow strict nesting rules
<table>
<div>Row wrapper</div> {/* Invalid */}
</table>
// CORRECT
<table>
<tbody>
<tr>
<td>Cell</td>
</tr>
</tbody>
</table>Find HTML validation errors — paste your HTML into the W3C validator or use browser DevTools → Elements → look for any auto-corrected nesting.
Fix 5: Handle Date and Random Values
Values that change between renders (time, random numbers) cause hydration mismatches:
// WRONG — time differs between server render and client hydration
function Greeting() {
const hour = new Date().getHours();
const greeting = hour < 12 ? 'Good morning' : 'Good afternoon';
return <p>{greeting}</p>;
// Server: "Good morning" (UTC 9:00)
// Client: "Good afternoon" (local 15:00) — mismatch
}
// CORRECT option 1 — render time-sensitive parts only on client
function Greeting() {
const [greeting, setGreeting] = useState('Hello'); // Generic server-safe default
useEffect(() => {
const hour = new Date().getHours();
setGreeting(hour < 12 ? 'Good morning' : 'Good afternoon');
}, []);
return <p>{greeting}</p>; // Server: "Hello", client: updates after mount
}
// CORRECT option 2 — pass the value as a prop from the server
// In Next.js App Router (Server Component):
export default function Page() {
const hour = new Date().getHours();
const greeting = hour < 12 ? 'Good morning' : 'Good afternoon';
return <Greeting initialGreeting={greeting} />;
// Server and client use the same greeting — no mismatch
}Random IDs — use a stable ID generator:
// WRONG — Math.random() differs server/client
function Item() {
const id = `item-${Math.random()}`; // Different every render
return <div id={id}>Content</div>;
}
// CORRECT — use React's useId() (React 18+)
import { useId } from 'react';
function Item() {
const id = useId(); // Stable, same on server and client
return <div id={id}>Content</div>;
}
// CORRECT — use a counter or UUID seeded from stable data
function Item({ index }: { index: number }) {
return <div id={`item-${index}`}>Content</div>;
}Fix 6: Handle Authentication and User State
Auth state often differs between server (not authenticated) and client (authenticated), causing hydration mismatches:
// WRONG — auth state differs server/client
function Header() {
const isLoggedIn = checkAuthCookie(); // Returns false on server, true on client
return (
<nav>
{isLoggedIn ? <UserMenu /> : <LoginButton />}
{/* Server renders LoginButton, client renders UserMenu — mismatch */}
</nav>
);
}Next.js App Router — pass auth state from server:
// app/layout.tsx (Server Component)
import { getServerSession } from 'next-auth';
export default async function Layout({ children }) {
const session = await getServerSession(); // Server-side auth check
return (
<html>
<body>
<Header session={session} /> {/* Same data on server and client */}
{children}
</body>
</html>
);
}
// Header.tsx (Client Component)
'use client';
function Header({ session }: { session: Session | null }) {
// Uses prop — no client-side auth check needed
return (
<nav>
{session ? <UserMenu user={session.user} /> : <LoginButton />}
</nav>
);
}Next.js Pages Router — use initial props:
export const getServerSideProps = async (context) => {
const session = await getSession(context);
return {
props: { initialAuth: !!session },
};
};
function Header({ initialAuth }: { initialAuth: boolean }) {
// Use initialAuth for first render — then client state takes over
const [isAuth, setIsAuth] = useState(initialAuth);
useEffect(() => {
// Sync with actual client-side auth state
checkClientAuth().then(setIsAuth);
}, []);
return <nav>{isAuth ? <UserMenu /> : <LoginButton />}</nav>;
}Fix 7: Debug Hydration Mismatches
React 18’s error message includes the expected vs. actual HTML. Use it to find the mismatch:
Warning: Expected server HTML to contain a matching <p> in <div>.
Hydration server mismatch:
Server: <div class="dark">...</div>
Client: <div class="light">...</div>Add data- attributes to narrow down the mismatch:
// Add a unique marker to each suspiciously dynamic section
<div data-section="theme-provider">
{children}
</div>
// If the error references this section, theme logic is causing the mismatchUse React DevTools — in development, React DevTools shows a warning icon on components with hydration mismatches. Clicking shows the specific prop or text content that differed.
Check for browser extensions — disable all extensions and reload. If the error disappears, an extension is modifying the DOM. Common culprits: Grammarly (adds attributes to inputs), Google Translate (rewrites text nodes), ad blockers (remove elements).
React 18 Strict Mode in development double-invokes renders — this can expose bugs that cause hydration mismatches by making them more visible:
// main.tsx
<React.StrictMode>
<App />
</React.StrictMode>
// Keep Strict Mode on — it helps catch bugs earlyStill Not Working?
Hydration error from third-party scripts — scripts that modify the DOM (chat widgets, analytics) before React hydrates cause mismatches. Load them asynchronously after hydration:
// Load third-party scripts after hydration
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://widget.example.com/chat.js';
script.async = true;
document.body.appendChild(script);
}, []);Next.js <Script> component with strategy="lazyOnload" or strategy="afterInteractive" ensures scripts load after hydration.
CSS-in-JS hydration — libraries like styled-components require server-side collection of styles to avoid mismatches. Follow each library’s SSR documentation.
Locale-dependent formatting — if formatting differs by server locale vs. client locale, pass locale explicitly:
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
// Specify locale explicitly — don't rely on system localeFor related React issues, see Fix: React useEffect Runs Twice and Fix: Next.js App Router Fetch Cache.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: nuqs Not Working — URL State Not Syncing, Type Errors, or Server Component Issues
How to fix nuqs URL search params state management — useQueryState and useQueryStates setup, parsers, server-side access, shallow routing, history mode, and Next.js App Router integration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.