How to Fix Next.js Hydration Error [2026 Complete Guide]
Are you suddenly seeing “Hydration failed because the initial UI does not match what was rendered on the server” or “Text content does not match server-rendered HTML” while developing with Next.js? This article provides a thorough 2026-updated guide covering the causes and concrete solutions for hydration errors, including the latest Next.js 15 environment.
What Is This Error? Symptoms You’ll See
The Next.js Hydration Error occurs when there is a mismatch between the HTML generated by server-side rendering (SSR) and the HTML that React attempts to render on the first client-side render.
What Is Hydration?
Hydration is the process where React takes the pre-rendered static HTML from the server and attaches event handlers and state to make it “interactive” in the browser. It’s called “hydration” because it injects interactivity—like adding water to bring something to life.
Common Error Messages
Here are the typical error messages developers encounter in the console or browser:
Hydration failed because the initial UI does not match what was rendered on the server.Text content does not match server-rendered HTML.Error: Hydration failed because the server rendered HTML didn't match the client.Warning: Expected server HTML to contain a matching <div> in <p>.
When Does It Occur?
This error primarily appears in the following situations:
- A red error appears in the browser console during Next.js app development
- Parts of a page don’t render correctly or interactions don’t work in production
- It’s often first noticed during post-build preview
- Chrome extensions modifying the DOM can also trigger it
In terms of impact, when a hydration error occurs, React attempts to completely rebuild the DOM on the client side, which can lead to performance degradation. Additionally, in Next.js 15 and later (React 19), hydration errors are handled more strictly—what were previously warnings may now be thrown as errors.
Why Does This Error Occur?
Hydration errors have many causes, but they can be broadly categorized into the following five groups.
Cause 1: Using Browser-Only APIs (window, localStorage, navigator)
Since the browser environment doesn’t exist on the server side, directly referencing browser-only objects like window, localStorage, navigator, or document in rendering logic produces different output on the server and client.
// ❌ BAD: window doesn't exist on the server, causing a mismatch
function MyComponent() {
const width = window.innerWidth; // Error or undefined on server
return <div>{width > 768 ? 'Desktop' : 'Mobile'}</div>;
}
A typical pattern is using typeof window !== 'undefined' conditional branching in rendering, which returns different JSX on the server and client, causing the error.
Cause 2: Using Dates, Times, and Random Values
Functions that return different values on each call—such as new Date(), Date.now(), Math.random(), and crypto.randomUUID()—will produce mismatched values between server and client when used during rendering.
// ❌ BAD: Different times displayed on server and client
function Clock() {
return <p>Current time: {new Date().toLocaleTimeString()}</p>;
}
Timezone differences are also a common cause. When the server runs in UTC and the client is in a local timezone, the same Date object produces different strings.
Cause 3: Invalid HTML Nesting
When the HTML structure violates nesting rules (e.g., placing a <div> inside a <p> tag), the browser auto-corrects the HTML, creating a mismatch between the server-sent HTML and the browser-interpreted DOM.
// ❌ BAD: <div> cannot be placed inside <p>
function BadNesting() {
return (
<p>
Text
<div>This part is the problem</div>
</p>
);
}
Browsers follow HTML parser rules and automatically restructure the DOM tree when invalid nesting is detected. This results in a different state between the HTML sent by the server and the DOM interpreted by the browser.
Cause 4: Chrome Extensions Modifying the DOM
As of 2026, one of the most frustrating causes for many developers is Chrome extensions. Extensions like ColorZilla, Grammarly, Google Translate, and password managers add attributes or elements to the DOM before React’s hydration begins, causing mismatches with the server HTML.
Especially in Next.js 15 + React 19 environments, attributes injected by extensions like cz-shortcut-listen="true" have been reported as direct causes of hydration errors.
Cause 5: Third-Party Scripts and Widget Interference
Third-party scripts such as Google Analytics, A/B testing tools, chat widgets, and ad scripts can modify the DOM before React hydration, triggering errors. These scripts add or modify elements in <head> or <body>, creating discrepancies with the original server-sent HTML.
Solution 1: Client-Only Rendering with useEffect (Recommended)
The most recommended solution is using the useEffect hook to set values only on the client side. Since useEffect runs after hydration is complete, it maintains HTML consistency between server and client.
Step 1: State and useEffect Combination Pattern
When you need browser-specific values (screen width, localStorage values, etc.), set the initial value to match the server, then update to client-specific values inside useEffect.
'use client';
import { useState, useEffect } from 'react';
function ResponsiveComponent() {
// Step 1: Set initial value to match server
const [isMobile, setIsMobile] = useState(false);
// Step 2: Set client-specific value inside useEffect
useEffect(() => {
setIsMobile(window.innerWidth < 768);
const handleResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div>
{isMobile ? <MobileNav /> : <DesktopNav />}
</div>
);
}
Step 2: Mount State Tracking Pattern
A more versatile pattern tracks whether the component has been mounted on the client side.
'use client';
import { useState, useEffect } from 'react';
function ClientOnlyComponent() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
// Before mount: same output as server (or skeleton)
if (!isMounted) {
return <div className="skeleton">Loading...</div>;
}
// After mount: display client-specific content
return (
<div>
<p>Current time: {new Date().toLocaleTimeString()}</p>
<p>Browser: {navigator.userAgent}</p>
</div>
);
}
Step 3: Create a Reusable Custom Hook
We recommend creating a custom hook for reuse across your project.
// hooks/useHydration.ts
'use client';
import { useState, useEffect } from 'react';
export function useHydrated() {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
setHydrated(true);
}, []);
return hydrated;
}
// Usage
function MyComponent() {
const hydrated = useHydrated();
if (!hydrated) return <Skeleton />;
return <div>{/* Client-specific content */}</div>;
}
Important Notes
- It’s crucial to maintain the same output as the server during the initial render of
useEffect - Use skeleton UI or loading indicators to minimize degradation of user experience
- This approach may affect SEO, so apply it carefully to content you want search engines to index
Solution 2: Disable SSR with Dynamic Import
When the useEffect pattern is inconvenient or when the entire component is client-only, you can disable SSR using Next.js’s dynamic().
Basic Usage
import dynamic from 'next/dynamic';
// Import with SSR disabled
const ClientOnlyChart = dynamic(
() => import('../components/Chart'),
{
ssr: false,
loading: () => <p>Loading chart...</p>
}
);
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<ClientOnlyChart />
</div>
);
}
Usage with App Router
With Next.js 13+ App Router, dynamic works the same way.
// app/dashboard/page.tsx
import dynamic from 'next/dynamic';
const MapComponent = dynamic(
() => import('@/components/Map'),
{ ssr: false }
);
export default function DashboardPage() {
return (
<main>
<h1>Map View</h1>
<MapComponent />
</main>
);
}
When This Approach Is Appropriate
- Components using map libraries (Leaflet, Google Maps)
- Components using chart libraries (Chart.js, D3.js)
- Components heavily dependent on browser APIs
- Editor components (Monaco Editor, CodeMirror, etc.)
Since SSR is completely disabled, the initial HTML won’t contain the component’s content. This method is not suitable for SEO-critical content.
Solution 3: Fix HTML Structure and Handle Browser Extensions (Advanced)
Fixing HTML Nesting Issues
If invalid HTML nesting is the cause, fix the structure to comply with HTML specifications.
// ❌ BAD: <div> cannot go inside <p>
<p>Text<div>Block element</div></p>
// ✅ OK: Wrap with <div> or use <span>
<div>
<p>Text</p>
<div>Block element</div>
</div>
// ✅ OK: Use inline elements
<p>Text<span>Inline element</span></p>
Special care is needed when embedding HTML from CMS or markdown using dangerouslySetInnerHTML. HTML from external sources often contains invalid nesting, so sanitization is recommended.
Handling Chrome Extensions
The following approaches are effective for hydration errors caused by extensions in development environments.
Method A: Restrict Extension Site Access
Right-click the Chrome extension icon and change “Read and change site data” to “When you click the extension.” This prevents extensions from modifying the DOM on localhost during development.
Method B: Create a Development-Only Browser Profile
Create a clean browser profile without extensions specifically for development to completely avoid extension-related hydration errors.
Method C: Use suppressHydrationWarning Selectively
Apply suppressHydrationWarning to elements where extensions commonly add attributes, such as the <body> tag.
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en">
<body suppressHydrationWarning={true}>
{children}
</body>
</html>
);
}
Important: suppressHydrationWarning only works one level deep—child element mismatches are still detected. It can also hide real bugs in production, so avoid overusing it.
Advanced: Using MutationObserver
This method removes attributes injected by extensions before hydration.
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
const attr = mutation.attributeName;
if (attr && attr.startsWith('cz-')) {
mutation.target.removeAttribute(attr);
}
}
});
});
observer.observe(document.documentElement, {
attributes: true,
subtree: true,
attributeFilter: ['cz-shortcut-listen']
});
})();
`,
}}
/>
</head>
<body suppressHydrationWarning={true}>
{children}
</body>
</html>
);
}
How to Prevent Hydration Errors
Here are best practices for preventing hydration errors proactively.
1. Avoid Environment-Dependent Code During Rendering
Don’t use code that generates different values depending on the environment inside your component’s return statement (rendering logic). Move all environment-dependent logic into useEffect.
2. Leverage CSS Media Queries
For responsive design switching, CSS Media Queries are the safest approach—far better than JavaScript conditional branching.
/* Prefer CSS over JavaScript conditional branching */
.mobile-only { display: none; }
.desktop-only { display: block; }
@media (max-width: 768px) {
.mobile-only { display: block; }
.desktop-only { display: none; }
}
3. Automate HTML Validation
Use ESLint’s jsx-a11y plugin and eslint-plugin-react rules to detect invalid HTML nesting at build time.
4. Establish a Testing Environment
To verify that hydration errors aren’t occurring in your development environment:
- Regularly test with
next build && next startfor production builds - Test with a clean browser profile that has no extensions
- Monitor hydration errors in production using error tracking tools like Sentry
5. Optimize Third-Party Script Placement
For Google Analytics, ad scripts, and similar resources, use the strategy property of Next.js’s <Script> component to load them at the appropriate timing.
import Script from 'next/script';
// Load after hydration
<Script src="https://example.com/analytics.js" strategy="afterInteractive" />
// Load during idle time
<Script src="https://example.com/widget.js" strategy="lazyOnload" />
Summary
Next.js Hydration Error is caused by mismatches between server and client rendering results. In the 2026 Next.js 15 + React 19 environment, these are checked more strictly than before, making proper understanding and handling increasingly important.
Key Takeaways:
- The useEffect pattern is the most effective solution for most cases. Always handle browser-only APIs and dynamic values inside
useEffect - Dynamic import (ssr: false) should be used when the entire component is client-dependent
- Always be mindful of correct HTML structure—avoid invalid nesting like placing
<div>inside<p> - For Chrome extension issues, use a development profile or
suppressHydrationWarning - Prioritize CSS Media Queries and avoid layout switching through JavaScript conditional branching
If the methods above don’t resolve your issue, try searching for your error message in Next.js GitHub Discussions. You may find solutions from other developers who faced the same problem. Additionally, implementing error monitoring tools like Sentry to continuously monitor hydration errors in production is highly recommended.
References
- Next.js Official Docs: Text content does not match server-rendered HTML
- GitHub Discussion: Hydration failed because the initial UI does not match
- GitHub Discussion: Hydration Error caused by chrome extension
- Next.js Hydration Errors in 2026: The Real Causes, Fixes, and Prevention Checklist (Medium)
- Sentry: Fixing Hydration Errors in server-rendered Components
- Resolving hydration mismatch errors in Next.js – LogRocket Blog

コメント