How to Fix Next.js Hydration Errors
On this page
Hydration errors are one of the most common — and most confusing — problems you'll run into when building a Next.js application. They tend to appear suddenly, often with a wall of red text in the console, and the message itself ("Hydration failed because the server rendered HTML didn't match the client") rarely points you to the exact line of code that caused it. This guide explains what hydration is, why these errors happen, and how to fix every common variation.
What Hydration Actually Is
When you use Next.js with the App Router or Pages Router, your components are first rendered to HTML on the server. That HTML is sent to the browser so the page paints quickly. Then React "hydrates" that markup on the client: it walks the existing DOM and attaches event handlers, state, and effects to make the page interactive.
Hydration works on one assumption: the HTML React generates on the client during the first render must match the HTML the server already sent. If the two trees differ, React can't reliably attach to the existing DOM. It throws a hydration error, discards the server HTML for the mismatched subtree, and re-renders on the client. The result is a flash of broken content, lost server-rendering benefits, and sometimes genuinely incorrect UI.
So fixing hydration errors is almost always about finding the one place where server output and client output diverge — and removing that divergence.
The Most Common Causes
Before jumping into fixes, it helps to recognize the usual suspects. The overwhelming majority of hydration errors come from one of these:
- Using browser-only APIs during render —
window,localStorage,navigator, ordocumentexist on the client but not the server. - Time- and date-dependent values —
new Date(),Date.now(), or formatting a timestamp produces different output on the server than milliseconds later on the client. - Random values —
Math.random(), generated IDs, or shuffled arrays differ between the two renders. - Invalid HTML nesting — a
<div>inside a<p>, or a<p>inside another<p>. The browser silently "fixes" the markup, which changes the DOM React expects. - Browser extensions — extensions that inject attributes (like Grammarly's
data-gr-*attributes) modify the DOM before React hydrates. - Locale or timezone differences — number and date formatting that depends on the server's locale versus the user's.
Fix 1: Guard Browser-Only Code
The single most frequent cause is reading window or localStorage during render. On the server those don't exist, so the rendered output differs.
The wrong way:
function Theme() {
// ❌ window is undefined on the server
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return <div className={isDark ? 'dark' : 'light'}>...</div>;
}
Move browser access into useEffect, which only runs on the client after hydration:
'use client';
import { useState, useEffect } from 'react';
function Theme() {
const [isDark, setIsDark] = useState(false); // matches server output
useEffect(() => {
setIsDark(window.matchMedia('(prefers-color-scheme: dark)').matches);
}, []);
return <div className={isDark ? 'dark' : 'light'}>...</div>;
}
The key idea: the first client render must produce the same markup as the server. Differences are allowed only after the effect runs.
Fix 2: Defer Rendering Until Mounted
When a component genuinely can't render meaningfully on the server (a chart that needs window dimensions, a component that reads localStorage), use a "mounted" flag to render nothing — or a placeholder — until the client takes over.
'use client';
import { useState, useEffect } from 'react';
function ClientOnly({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null; // server and first client render both produce null
return children;
}
Because both the server and the first client render return null, they match. After mounting, the real content appears. This is a clean, reusable pattern.
Fix 3: Use next/dynamic With SSR Disabled
For an entire component that should never render on the server, skip the manual flag and use dynamic imports:
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('./Map'), { ssr: false });
This tells Next.js not to render Map on the server at all, eliminating any chance of mismatch. Use this for heavyweight client-only widgets like maps, editors, and charts. Note that in the App Router, ssr: false is only allowed in Client Components.
Fix 4: Handle Dates and Random Values Deterministically
Times and random numbers are mismatch machines. The server renders at one instant; the client hydrates at another.
// ❌ Server time ≠ client time
<span>{new Date().toLocaleTimeString()}</span>
Render dynamic time values inside useEffect, or render a stable placeholder server-side and fill in the live value after mount. For formatting an existing timestamp (like a "posted at" date), pass an explicit locale and timezone so both environments agree:
new Intl.DateTimeFormat('en-US', { timeZone: 'UTC' }).format(date);
For random IDs needed for accessibility, use React's built-in useId() hook, which generates IDs that are stable across server and client:
const id = useId();
Fix 5: Correct Invalid HTML Nesting
React's error overlay sometimes points at a mismatch caused purely by invalid HTML. The browser auto-corrects illegal nesting before React hydrates, so the DOM no longer matches.
Common offenders:
<p><div>...</div></p>— block elements aren't allowed inside<p>.<a><a>...</a></a>— nested anchors.<table>without<tbody>(the browser inserts one).- A
<button>inside another<button>.
The fix is simply to write valid HTML. If a styled component renders a <p>, don't put a <div> inside it — use a <span> or restructure.
Fix 6: Suppress Unavoidable Mismatches
Occasionally a mismatch is expected and harmless — for example, rendering a timestamp you know will differ, or working around a browser extension. React provides suppressHydrationWarning for exactly this:
<time suppressHydrationWarning>{new Date().toISOString()}</time>
Use this sparingly. It only suppresses the warning one level deep, and it does not fix actual logic bugs — it just tells React "I know this differs, don't warn me." For extension-injected attributes on <html> or <body>, this is the standard escape hatch.
A Practical Debugging Workflow
When you hit a hydration error and can't immediately see the cause:
- Read the diff. Modern React and Next.js error overlays show the specific element and attribute that mismatched. This is your strongest clue.
- Test in incognito mode with extensions disabled. If the error vanishes, a browser extension is the culprit — suppress it on the affected element.
- Search for browser globals. Grep your codebase for
window,document,localStorage,Date.now, andMath.randomused outside ofuseEffect. - Binary-search the tree. Comment out half your component subtree, reload, and narrow down which half triggers the error.
- Check third-party libraries. Some libraries read
windowduring render; wrap them inClientOnlyor load them withssr: false.
Prevention Tips
The best fix is never writing the bug. Keep these habits:
- Treat the first render as server-deterministic — no clocks, no randomness, no browser APIs.
- Push all client-only logic into
useEffect. - Validate your HTML structure, especially around
<p>,<a>,<table>, and<button>. - Always pass explicit
localeandtimeZoneto date and number formatters. - Reserve
suppressHydrationWarningfor genuinely unavoidable, harmless differences.
FAQ
Why do hydration errors only show up sometimes?
Many causes are timing- or environment-dependent. A Date.now() mismatch might be invisible if the server and client render within the same millisecond, and an extension-based error won't appear in incognito. This intermittency is exactly why they're frustrating — reproduce them in a clean environment to be sure.
Does suppressHydrationWarning hurt performance or SEO?
No. It only silences the console warning for that element's text or attributes. It doesn't disable server rendering or change what crawlers see. It's safe when the mismatch is genuinely expected, but it won't repair a real logic error.
Is ssr: false bad for SEO?
It can be, because that component's content won't be in the server-rendered HTML. Use it only for interactive widgets that don't carry indexable content (maps, charts, editors). For anything search engines need to read, fix the mismatch properly instead.
Why does my error mention Grammarly or data-* attributes I never wrote?
A browser extension injected those attributes into the DOM before React hydrated. Disable extensions to confirm, then add suppressHydrationWarning to the affected element (often <body>), since you can't control what extensions do.
Are hydration errors different in the App Router versus the Pages Router?
The underlying cause is identical — server and client output diverged. The App Router adds Server Components (which never hydrate) and restricts ssr: false to Client Components, but the fixes above apply to both routers.
Can I just ignore hydration warnings? Don't. Even when the page looks fine, React is throwing away server HTML and re-rendering the mismatched subtree, which costs performance and can produce subtly wrong UI. Treat every hydration error as a real bug to track down.