Hydration Failed Because the Initial UI Does Not Match: Fixes
Why React reports a hydration mismatch, the usual causes (dates, window, invalid nesting, extensions), and a safe fix for each with examples.

The error means React built your page twice and got two different results. The server (or the build step) turned your components into HTML. Then, in the browser, React rendered the same components again so it could attach event handlers, and that output didn't line up with the HTML it was given. React calls this step hydration, and when the two trees differ it reports a hydration mismatch.
Almost every case comes from one of four things: code that reads window, localStorage, or another browser-only API during render; values that change between runs, like dates and random numbers; HTML that browsers silently rewrite, like a <div> inside a <p>; or something outside your code editing the HTML, like a browser extension or a CDN minifier. The fixes below go from the most common and safest to the last-resort escape hatch.
Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: Text content does not match server-rendered HTML.
In development, React warns about each mismatch, and the warning usually points at the text or attribute that differs. Read that first. It tells you which of the fixes below you need.
What hydration failed because the initial UI does not match means#
Server rendering produces an HTML snapshot so the user sees something before JavaScript loads. Hydration is when React takes that snapshot, renders the component tree once more in the browser, and attaches event handlers to the existing DOM nodes instead of creating new ones. That only works if the second render produces the same structure, text, and attributes as the first.
Per the React hydrateRoot reference, React treats mismatches as bugs. In development it warns on every one. It recovers from some mismatches, but that costs time, and in the worst case event handlers get attached to the wrong elements. Attribute differences aren't guaranteed to be patched at all, because checking every attribute on every node would be too slow. So the goal is never "make the warning go away". It's "make both renders agree".
The Next.js hydration error page lists the usual causes:
- Incorrect HTML nesting:
<p>inside<p>,<div>or<ul>inside<p>,<a>inside<a>,<button>inside<button>. - Checks like
typeof window !== 'undefined'in render logic. - Browser-only APIs such as
window,localStorage, orwindow.matchMediain render logic. - Time-dependent APIs like the
Date()constructor in render logic. - Browser extensions that modify the HTML.
- Misconfigured CSS-in-JS libraries.
- An edge or CDN layer that rewrites the HTML response, such as Cloudflare Auto Minify.
Here's the date case in isolation. The same instant formats as two different dates depending on the timezone, which is exactly what happens when a server in one region renders a page for a user in another.
// One instant in time, formatted the way a server and a browser might each see it.
const instant = new Date('2026-10-15T23:30:00Z');
const serverText = instant.toLocaleDateString('en-US', { timeZone: 'UTC' });
const clientText = instant.toLocaleDateString('en-US', { timeZone: 'Asia/Jakarta' });
console.log('server rendered:', serverText);
console.log('client rendered:', clientText);
console.log('hydration would match:', serverText === clientText);
// Random values fail for the same reason: every render produces a new one.
const serverId = 'id-' + Math.random().toString(36).slice(2, 8);
const clientId = 'id-' + Math.random().toString(36).slice(2, 8);
console.log('random ids match:', serverId === clientId);
Fix 1: render the same thing first, then change it in useEffect#
This is the fix for window, localStorage, matchMedia, navigator, dates, and random values. The rule is simple: the first client render must return exactly what the server returned. Anything that depends on the browser goes into an effect, which runs after hydration and triggers a second render.
The smallest version, from the Next.js docs:
import { useState, useEffect } from 'react'
export default function App() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
return <h1>{isClient ? 'This is never prerendered' : 'Prerendered'}</h1>
}
Both the server and the first client render see isClient === false, so they match. The effect then flips it and React re-renders with the browser-only content.
A more realistic variation, a theme label read from localStorage:
import { useState, useEffect } from 'react'
export default function ThemeLabel() {
// Same default on the server and in the first client render.
const [theme, setTheme] = useState('light')
useEffect(() => {
const saved = window.localStorage.getItem('theme')
if (saved) setTheme(saved)
}, [])
return <span>Theme: {theme}</span>
}
The same shape works for "is this a narrow viewport" with window.matchMedia, for "what time is it in the user's zone", and for anything random. Compute it in the effect, store it in state, start from a neutral default. If your framework separates server and client components, this one has to be a client component since it uses hooks.
What not to do is the typeof window guard:
// Wrong: the server takes one branch, the browser takes the other.
const width = typeof window !== 'undefined' ? window.innerWidth : 0
return <p>Width: {width}</p>
It looks defensive, but it guarantees a mismatch, because the check itself evaluates differently in the two environments. Both the React and Next.js docs call it out by name.
One caveat from the React docs: this pattern renders the component twice, so hydration gets slower, and content that swaps right after load can feel jarring on slow connections. Keep the default state as close to the final one as you can.
How to confirm: reload the page in development. The warning should be gone and the console clean. Then check that the page still shows the browser-dependent value after a moment, which proves the effect ran.
Fix 2: repair invalid HTML nesting#
Browsers don't render the HTML you wrote. They parse it, and the parser repairs certain mistakes by closing or moving tags. A <p> can only contain phrasing content, so when the parser meets a <div> inside a <p> it closes the paragraph first. React then compares its own tree, which still has the <div> inside the <p>, against a DOM where it isn't, and reports a mismatch.
Common offenders: a link that wraps a card containing another link; a <button> inside a <button>; a block-level component dropped into a paragraph; a <ul> inside a <p>. MDN's <p> element reference lists what's allowed inside a paragraph.
Before and after:
// Wrong: <div> inside <p>, and a nested <a>.
<p>
<div>Shipping details</div>
<a href="/cart">Cart <a href="/help">help</a></a>
</p>
// Fixed: block content gets a block parent, links don't nest.
<div>
<div>Shipping details</div>
<a href="/cart">Cart</a> <a href="/help">help</a>
</div>
A small checker for the two rules behind most of these reports:
// Block tags can't live inside <p>, and some tags can't contain themselves.
const notInsideP = new Set(['p', 'div', 'ul', 'ol', 'h1', 'h2', 'h3', 'table', 'section']);
const noSelfNest = new Set(['a', 'button', 'p', 'form']);
function checkNesting(html) {
const problems = [];
const stack = [];
const tagRe = new RegExp('</?([a-z0-9]+)[^>]*>', 'gi');
let m;
while ((m = tagRe.exec(html)) !== null) {
const tag = m[1].toLowerCase();
if (m[0].startsWith('</')) {
const open = stack.lastIndexOf(tag);
if (open !== -1) stack.length = open;
continue;
}
if (stack.includes('p') && notInsideP.has(tag)) problems.push(`<${tag}> inside <p>`);
if (noSelfNest.has(tag) && stack.includes(tag)) problems.push(`<${tag}> inside <${tag}>`);
stack.push(tag);
}
return problems;
}
const samples = [
'<p>Shipping: <div>two days</div></p>',
'<a href="/cart">Cart <a href="/help">help</a></a>',
'<p>Fine <strong>here</strong></p>',
];
for (const html of samples) {
const found = checkNesting(html);
console.log(found.length ? 'PROBLEM: ' + found.join(', ') : 'ok', '|', html);
}
Watch for the indirect version too: your JSX looks fine, but a child component renders a <div> and you placed that component inside a <p>. Inspect the rendered DOM in the browser's dev tools, not just your source.
How to confirm: the warning no longer mentions a tag that moved, and inspecting the element in dev tools shows the same parent your JSX has.
Fix 3: render the component only on the client with next/dynamic#
Sometimes a component simply can't be server rendered: it wraps a browser-only library, draws to a canvas, or reads from storage in a dozen places. Next.js lets you turn off prerendering for just that component:
import dynamic from 'next/dynamic'
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })
export default function Page() {
return (
<div>
<NoSSR />
</div>
)
}
With ssr: false, the server renders nothing for that slot, so there's nothing to mismatch. The cost is that the component's content isn't in the HTML at all, which matters for anything that should be visible immediately or indexed. I'd use it for widgets, not for the page body.
How to confirm: view the page source, not the dev tools DOM. The component's markup should be absent from the HTML and appear only after JavaScript runs.
Fix 4: suppressHydrationWarning for unavoidable differences#
For a single element whose text or attribute will always differ, like a timestamp shown in the viewer's local time, React provides an escape hatch:
<time dateTime="2016-10-25" suppressHydrationWarning>
{new Date().toLocaleString()}
</time>
Two limits, stated in both the React and Next.js docs:
- It only works one level deep. It does not silence warnings in children.
- React will not patch mismatched text content when it's set. The server's text stays in place.
So this hides the warning, it doesn't reconcile the content. Use it for values where briefly showing the server's version is acceptable, and prefer Fix 1 for anything users interact with. Don't overuse it; the docs are explicit on that point.
How to confirm: the warning disappears for that element only. If it persists, the mismatch is in a child or a sibling, and you need one of the other fixes.
Causes outside your code: extensions, CDNs, and iOS#
If the warning shows attributes or wrapper elements you never wrote, the HTML is being modified somewhere between your server and React.
- Browser extensions. Password managers, translators, and ad blockers inject attributes and nodes. Reload in a private window with extensions disabled. If the error vanishes, it isn't your bug. You can't fix it in code, but keeping your own tree clean makes real problems stand out.
- Edge and CDN rewriting. Anything that minifies or rewrites HTML on the way out can change whitespace and attributes. Cloudflare Auto Minify is the example the Next.js docs name. Turn off HTML minification at that layer for the app's routes.
- iOS data detection. Safari on iOS turns phone numbers, emails, dates, and addresses in text into links, which changes the DOM. Disable it with a meta tag in the document head:
<meta
name="format-detection"
content="telephone=no, date=no, email=no, address=no"
/>
- CSS-in-JS. A styling library that isn't wired into server rendering emits different class names or style tags on each side. Follow the library's official Next.js example rather than a custom setup.
How to confirm: compare the page source with the DOM in dev tools. Markup present in the DOM but absent from the source was injected after the response left your server.
Common mistakes that keep the error coming back#
- Sprinkling suppressHydrationWarning everywhere. It's one level deep and it hides real bugs. If you need it on more than a couple of elements, the actual problem is upstream.
- Guarding with
typeof windowortypeof document. This is the cause, not the fix. Move the read intouseEffect. - Using
Date.now()orMath.random()for keys or IDs. Every render produces a new value. Use a stable identifier from your data, or React'suseIdfor generated IDs. - Calling
new Date().toLocaleDateString()in render. Even for the same instant, timezone and locale differ between server and browser. Format in an effect, or pass a preformatted string down from the server. - Fixing the JSX but not the DOM. Nesting bugs often come from a child component. Check the live DOM tree.
- Extra whitespace around the root node. The React docs list newlines around the React-generated HTML inside the root container as a cause when you call
hydrateRootyourself. - Only testing on your own machine. If your server and browser share a timezone and locale, date bugs stay hidden until a user in another region loads the page.
- Not reporting it in production. The warnings are a development feature. If you call
hydrateRootdirectly, passonRecoverableErrorso mismatches that reach real users get logged:
import { hydrateRoot } from 'react-dom/client'
hydrateRoot(document.getElementById('root'), <App />, {
onRecoverableError: (error, errorInfo) => {
reportToYourService({ error, componentStack: errorInfo.componentStack })
},
})
Quick checklist#
- Read the warning. Note whether it's text, an attribute, or a tag in the wrong parent.
- Text or attribute differs and involves
window, storage, dates, or random values: move the read intouseEffectwith a neutral default (Fix 1). - A tag has a different parent than your JSX: fix the nesting, checking the live DOM and not just the source (Fix 2).
- The component can't run on the server at all:
dynamic(..., { ssr: false })(Fix 3). - One element will always differ and that's acceptable:
suppressHydrationWarning, one level deep (Fix 4). - Attributes or wrappers you never wrote: test in a private window, check CDN minification, add the iOS
format-detectionmeta tag. - Report with
onRecoverableErrorin production so you hear about the mismatches you can't reproduce locally.
Quick answers
What does "hydration failed because the initial UI does not match what was rendered on the server" mean?
React rendered your components on the server to produce HTML, then rendered them again in the browser to attach event handlers, and the two outputs differed. The most common causes are browser-only APIs, dates or random values in render logic, and invalid HTML nesting.
Is suppressHydrationWarning a safe fix?
It's an escape hatch, not a fix. It only works one level deep, and React won't patch the mismatched text, so the server's version stays. Use it for a single unavoidable difference like a timestamp and fix everything else properly.
Why does the hydration error only happen for some users?
Usually a browser extension is modifying the HTML, a CDN is minifying it, or iOS Safari is turning phone numbers and dates into links. Test in a private window, check your edge layer, and add the format-detection meta tag for iOS.
Does a hydration mismatch break the page?
React recovers from some mismatches by re-rendering, but the docs say to treat them as bugs. In the best case the page gets slower; in the worst case event handlers attach to the wrong elements.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note