How to Fix Next.js Hydration Errors: Complete Debugging Guide
On this page
"Hydration failed because the initial UI does not match what was rendered on the server." If you've worked with Next.js for any length of time, you've seen this error. It's one of the most common and most frustrating errors in the framework, and the error message itself is maddeningly vague about what actually went wrong. I've spent hours tracking these down on various projects, so here's everything I know about why it happens and how to fix every common cause.
TL;DR: Learn why Next.js hydration errors happen and how to fix every common cause including date rendering, browser extensions, and conditional logic.
What Is Hydration?
When Next.js renders a page on the server, it produces HTML that gets sent to the browser. React then "hydrates" that HTML by attaching event listeners and making it interactive. Hydration expects the server-rendered HTML to exactly match what React would render on the client.
When they do not match, React throws a hydration error. The page still works (React recovers by re-rendering), but the error degrades performance and indicates a real bug.
Cause 1: Date and Time Rendering
This is the most common cause. The server renders a date in one timezone, the client renders it in another:
// This WILL cause a hydration error
function Timestamp() {
return <p>Last updated: {new Date().toLocaleString()}</p>;
}
The server might render "5/30/2026, 2:00:00 PM" (UTC+8) while the client renders "5/30/2026, 6:00:00 AM" (UTC).
Fix: Render Dates Client-Side
'use client';
import { useState, useEffect } from 'react';
function Timestamp() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <p>Last updated: Loading...</p>;
}
return <p>Last updated: {new Date().toLocaleString()}</p>;
}
Or better yet, use the suppressHydrationWarning prop for non-critical timestamps:
function Timestamp() {
return (
<time suppressHydrationWarning>
{new Date().toLocaleString()}
</time>
);
}
Cause 2: Browser Extensions
Extensions like Grammarly, password managers, ad blockers, and translation tools inject elements into the DOM before React hydrates. This creates a mismatch.
Fix: Target Body-Level Injection
Add suppressHydrationWarning to your root layout body tag:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body suppressHydrationWarning>{children}</body>
</html>
);
}
This only suppresses the warning one level deep (the body element itself), not all children. It handles most extension-related issues without masking real bugs.
Cause 3: Conditional Rendering Based on Client State
Rendering different content based on window, localStorage, or other browser-only APIs:
// This WILL cause a hydration error
function Greeting() {
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme')
: 'light';
return <div className={theme === 'dark' ? 'bg-black' : 'bg-white'}>
Hello
</div>;
}
The server always renders bg-white. The client might render bg-black.
Fix: Use useEffect for Client-Only State
'use client';
import { useState, useEffect } from 'react';
function Greeting() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved);
}, []);
return (
<div className={theme === 'dark' ? 'bg-black' : 'bg-white'}>
Hello
</div>
);
}
Cause 4: Invalid HTML Nesting
React is strict about HTML nesting rules. A <div> inside a <p> tag, or a <p> inside another <p>, will cause hydration errors because the browser auto-corrects the HTML before React hydrates:
// This WILL cause a hydration error
function Article() {
return (
<p>
Some text
<div>This div inside a p tag is invalid HTML</div>
</p>
);
}
Fix: Use Valid HTML Nesting
function Article() {
return (
<div>
<p>Some text</p>
<div>This is now properly nested</div>
</div>
);
}
Common invalid nesting pairs to watch for:
<p>cannot contain<div>,<h1>-<h6>,<ul>,<ol>, or another<p><a>cannot contain another<a><button>cannot contain another<button>
Cause 5: Random Values and IDs
Using Math.random() or crypto.randomUUID() during render produces different values on server vs client:
// This WILL cause a hydration error
function RandomId() {
const id = Math.random().toString(36).substring(7);
return <input id={id} />;
}
Fix: Use React's useId Hook
import { useId } from 'react';
function StableId() {
const id = useId();
return <input id={id} />;
}
useId generates deterministic IDs that match between server and client.
Cause 6: Third-Party Scripts and Widgets
Chat widgets, analytics scripts, and embedded content often modify the DOM:
Fix: Load Third-Party Scripts After Hydration
'use client';
import { useEffect, useState } from 'react';
function ChatWidget() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
if (!loaded) return null;
return <div id="chat-widget-container" />;
}
Or use Next.js's Script component with the afterInteractive strategy:
import Script from 'next/script';
function ChatWidget() {
return (
<Script
src="https://chat-widget.example.com/widget.js"
strategy="afterInteractive"
/>
);
}
Debugging Hydration Errors
When the error message is not clear, here is a systematic approach:
Step 1: Check the Browser Console
Next.js shows detailed hydration mismatch information in development. Look for the "Expected server HTML to contain" message which tells you exactly which elements differ.
Step 2: Binary Search with Suppression
Add suppressHydrationWarning to suspect elements one at a time until the error disappears. This identifies the source without fixing the root cause.
Step 3: Diff Server vs Client Output
'use client';
import { useEffect, useRef } from 'react';
function HydrationDebugger({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current) {
console.log('Client HTML:', ref.current.innerHTML);
}
}, []);
return <div ref={ref}>{children}</div>;
}
Compare the console output with the page source (View Source in browser).
Pro Tips
-
Never use
suppressHydrationWarningas a blanket fix. It should only be used for intentional mismatches like timestamps or locale-specific content. -
The
'use client'directive does not prevent hydration errors. Client components are still server-rendered and hydrated. The directive only means the component can use hooks and browser APIs. -
Next.js development mode shows detailed hydration info that production mode suppresses. Always debug hydration issues in development.
-
React 19's improved hydration error messages show a diff of the expected vs actual HTML. Make sure you are on the latest React version for better debugging.
Key Takeaways
- Hydration errors mean server and client render different HTML
- Dates, browser APIs, random values, and invalid HTML are the usual suspects
- Use
useEffectto defer client-only rendering - Use
useIdinstead of random IDs - Use
suppressHydrationWarningsparingly and intentionally - Always debug in development mode where error messages are detailed
Sources
Looking for more? Check out Adaptels.
Related Articles
Next.js vs Remix in 2026: Which React Framework Should You Pick?
Compare Next.js and Remix in 2026: routing, performance, data loading, DX, and deployment. Which framework wins for your project?
Next.js vs Remix in 2026: Which React Framework Should You Choose?
A detailed comparison of Next.js and Remix in 2026 covering performance, DX, data loading, deployment, and when to pick each framework.
How to Find and Fix Memory Leaks in Node.js Applications
Learn to detect, diagnose, and fix memory leaks in Node.js with heap snapshots, profiling tools, and common patterns that cause leaks.