Quick answer
Fix a Next.js hydration error by making the server HTML and the first browser render produce the same tree, text, attributes, and IDs. Move reads of localStorage, window, media queries, local time, and random values out of render; pass stable server data; repair invalid HTML; then rule out extensions, CDN transforms, and CSS-in-JS configuration.
The Next.js error usually appears as one of these messages:
Hydration failed because the initial UI does not match what was rendered on the server.
Warning: Text content did not match. Server: "..." Client: "..."React is not complaining that an effect changed the page after load. It is reporting a more specific contract failure: the HTML from the server differs from the output React produces for its first render in the browser.
That distinction matters. A fast workaround often makes the warning disappear while leaving an incorrect DOM, a flash of the wrong state, or an entire client-rendered subtree in production. The reliable fix is to find the non-deterministic input and give both sides the same initial result.
Quick Answer
Fix a Next.js hydration error by making the server HTML and the first browser
render produce the same tree, text, attributes, and IDs. Move reads of
localStorage, window, media queries, local time, and random values out of
render; pass stable server data; repair invalid HTML; then rule out extensions,
CDN transforms, and CSS-in-JS configuration.
TL;DR
- Hydration attaches React behavior to server HTML. It expects an identical first client render.
"use client"does not mean browser-only initial render. Client Components can still be rendered into the initial server response.- Use
useEffectfor browser-only preferences and APIs. Render a stable fallback first. - Pass an ISO timestamp, request snapshot, feature flag, or session-derived value from the server rather than calculating a different one on each side.
- Use
useIdfor rendered accessibility IDs. Never useMath.random()or an incrementing module variable for them. - Invalid HTML can be repaired by the browser before React sees it. Compare View Source with the Elements panel.
- Treat
suppressHydrationWarningas a narrow leaf-level escape hatch, not a root-layout fix.
Symptoms
| What you see | What it usually means | First place to look |
|---|---|---|
Hydration failed because the initial UI does not match | The DOM shape, element type, or rendered branch differs | Conditional rendering and invalid HTML nesting |
Text content did not match | Both sides rendered a text node with different values | Date, locale, user data, random value, or cache |
Attribute mismatch such as className or data-* | Server and client computed different attributes | Theme, CSS-in-JS, feature flags, extension-added attributes |
| Error only on a real phone | The browser or device changes markup or environment data | iOS linkification, locale, timezone, extension-like injected tools |
| Error only after deployment | Production input or response mutation differs from local development | CDN, cookies, edge middleware, build configuration, CSS extraction |
| Page becomes interactive late or loses state | React recovered by replacing part of the tree | Fix the first mismatch; do not ignore the warning |
Common Causes
| Cause | Why server and client differ | Durable repair |
|---|---|---|
new Date(), Date.now(), relative time | Different clock, timezone, locale, or render moment | Pass a stable ISO value; format locally after hydration if needed |
Math.random() or crypto.randomUUID() in render | A new value is created in each environment | Pass a stable ID from data or use useId for accessibility wiring |
localStorage, sessionStorage, window | The server has no browser state; a guarded branch can still differ | Render a shared default, then read in useEffect |
matchMedia, viewport width, navigator | Browser device state is unavailable or different on the server | Prefer CSS for layout; defer behavior to an effect |
| Invalid HTML nesting | The browser parser rewrites the server HTML into a different DOM tree | Use valid, semantic nesting and inspect parsed DOM |
| Cookies, auth state, A/B flags, live data | The browser uses a different cache or stale source than the request | Make the server request snapshot the initial client prop |
| CSS-in-JS class generation | Hash or insertion order differs between server and client | Follow the library's Next.js SSR integration exactly |
| Extension, translation, CDN, edge transform | Something changes the DOM or response before hydration | Reproduce cleanly; disable HTML rewriting and isolate third-party scripts |
Root Cause: Hydration Compares Two Initial Snapshots
Server-side rendering gives the visitor visible HTML before the JavaScript bundle arrives. Hydration is the browser step where React takes ownership of that existing markup and attaches event handlers and state to it.
React expects the component tree passed to hydration to produce the same
output as the tree that produced the server response. It does not exhaustively
repair every difference because doing that validation and repair on every page
would be expensive. A mismatch can range from a warning to a recoverable
client re-render; in the worst case, logic can attach to the wrong element.
React's hydrateRoot reference
is clear: treat mismatches as bugs.
The rule: deterministic until hydration completes
An initial render may depend on:
- route parameters and data fetched for that request;
- cookies or headers deliberately read on the server;
- props serialized from Server Components to Client Components; and
- constants committed with the build.
It must not depend on values that are different by location, moment, browser profile, or DOM mutation. The page can absolutely change after hydration. The important boundary is that it changes after both environments have agreed on the starting snapshot.
Why "use client" does not prevent this
In the Next.js App Router, a Client Component can use state, effects, and event handlers. Next.js still renders its initial HTML as part of the server response. Therefore, this component has a hydration risk:
"use client"
export function AccountGreeting() {
const name =
typeof window === "undefined"
? "Guest"
: window.localStorage.getItem("name") || "Guest"
return <p>Welcome, {name}</p>
}The server emits Welcome, Guest. A browser that has "Asha" in storage
renders Welcome, Asha before hydration. The typeof window guard prevents a
server crash; it does not make both render passes equal.
This behavior applies to the Pages Router too. The APIs differ, but the server-rendered response and first browser render must still agree.
Step-by-Step Solution
Step 1: Reproduce the mismatch in a production-shaped run
Development overlays are helpful, but start by confirming the deployed behavior. Build the same app configuration used in CI, serve it, and use a private browser window with extensions disabled:
npm run build
npm run startOpen the affected route and preserve the browser console. Then compare:
- View Source: the raw HTML response sent by Next.js or your CDN.
- Elements: the DOM after the browser parser and any early scripts or extensions have touched it.
- The React component stack in the error: the first named component is usually closer to the cause than the page root.
If View Source already contains the wrong value, debug server data or server rendering. If View Source is correct but Elements differs before React runs, look for invalid markup, an extension, browser linkification, injected scripts, or a response transform.
Step 2: Search for non-deterministic render inputs
Search is faster than inspecting every component manually. This query finds common inputs; each result is a lead, not automatically a bug.
rg -n --glob '*.{ts,tsx,js,jsx}' \
'Date\(|Date\.now|Math\.random|randomUUID|localStorage|sessionStorage|window\.|document\.|navigator\.|matchMedia|toLocale' \
app components pagesFor every hit, ask one question: Can this expression change the returned JSX
before useEffect runs? If yes, either make it a stable prop or defer it.
Step 3: Make time and locale output stable
This minimal example fails when server and browser clocks, timezones, or locale defaults differ:
"use client"
export function OrderPlacedAtBad() {
return <time>{new Date().toLocaleString()}</time>
}Do not assume that one deployment region means one timezone. A Docker image may be configured for UTC while a visitor uses India Standard Time; different Node builds can also carry different ICU locale data.
Pass a value that belongs to the request or database record. Render the same stable fallback on both sides, then format for the visitor after hydration:
"use client"
import { useEffect, useState } from "react"
type EventTimeProps = {
iso: string
}
export function EventTime({ iso }: EventTimeProps) {
const [label, setLabel] = useState(iso)
useEffect(() => {
const formatter = new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
})
setLabel(formatter.format(new Date(iso)))
}, [iso])
return <time dateTime={iso}>{label}</time>
}The server and initial client render both output the exact iso string. The
effect runs only in the browser and changes it to the visitor's local format.
The tradeoff is a small text update after hydration. For highly visible times,
reserve enough width or display a neutral skeleton so the update does not
cause layout shift.
Step 4: Read browser storage and media state after hydration
This is a common incorrect fix. It avoids calling localStorage on the
server but still chooses a different first render in the browser:
"use client"
import { useState } from "react"
export function ThemeLabelBad() {
const [theme] = useState(() => {
if (typeof window === "undefined") return "light"
return window.localStorage.getItem("theme") || "light"
})
return <p data-theme={theme}>Theme: {theme}</p>
}Use a common default, then update it in an effect:
"use client"
import { useEffect, useState } from "react"
type Theme = "light" | "dark"
export function ThemeLabel() {
const [theme, setTheme] = useState<Theme>("light")
useEffect(() => {
const stored = window.localStorage.getItem("theme")
setTheme(stored === "dark" ? "dark" : "light")
}, [])
return <p data-theme={theme}>Theme: {theme}</p>
}Expected initial HTML and initial client output:
Theme: lightAfter hydration, a saved dark preference changes the label to Theme: dark
without a mismatch.
This produces a flash if the preference controls the entire page theme. For that case, use a theme solution designed for Next.js, or set an intentional pre-hydration document class with a matching strategy. Do not broadly suppress the warning on the document as a substitute for making child components deterministic.
For responsive layout, prefer CSS media queries so both sides render the same
DOM. A JavaScript branch on matchMedia() that chooses a mobile navigation or
a desktop navigation before hydration is fragile. Use an effect only when
browser capability changes behavior, not merely styling.
Step 5: Use useId or data IDs, never random rendered IDs
This code creates different htmlFor and id values in the two environments:
export function EmailFieldBad() {
const id = "email-" + Math.random()
return (
<div>
<label htmlFor={id}>Email address</label>
<input id={id} type="email" autoComplete="email" />
</div>
)
}For an accessibility relationship local to a component, React's useId is the
correct tool:
"use client"
import { useId } from "react"
export function EmailField() {
const id = "email-" + useId()
return (
<div>
<label htmlFor={id}>Email address</label>
<input id={id} type="email" autoComplete="email" />
</div>
)
}useId derives IDs from the component's position in an identical tree, which
makes it safe for server rendering. It is not a list-key generator and cannot
rescue a component tree that itself changes between server and client. See the
React useId reference for the
multi-root identifierPrefix requirement when separate React applications
share one page.
Step 6: Repair invalid HTML before blaming React
Browsers parse invalid HTML according to their own rules. For example, a
paragraph cannot contain a div; the browser may implicitly close the
paragraph before the div. React then attempts to hydrate a DOM shape that is
not the one its JSX describes.
export function ReleaseNoteBad() {
return (
<p>
Release notes:
<div>Deployment completed.</div>
</p>
)
}Use a valid structure:
export function ReleaseNote() {
return (
<section>
<p>Release notes:</p>
<div>Deployment completed.</div>
</section>
)
}Also check nested interactive elements, such as a button inside a button or an anchor inside an anchor. The Next.js hydration error guide lists invalid nesting as a first-class cause because parser repair happens before React can reconcile the tree.
Step 7: Keep request data as one initial snapshot
An authenticated header, experiment variant, locale, feature flag, inventory count, or currency should have one source for the server response and first client render. A frequent production bug is:
- the server renders from the request cookie;
- the client immediately renders from a stale
localStoragecache; and - the page starts with two different users or variants.
Pass the server's initial snapshot as a serializable prop. The client can revalidate after hydration, but it should not replace the first render with a different cache value. This is also a security boundary: do not let a client-controlled display cache decide authorization-sensitive server content.
Choose the Right Repair, Not the Shortest One
| Strategy | Best for | Benefit | Cost or caveat |
|---|---|---|---|
| Stable server prop | Request data, timestamps, flags, authenticated UI | Correct SSR, SEO, and no mismatch | Requires clear data ownership |
useEffect after stable fallback | Storage, browser APIs, locale formatting | Keeps SSR and makes browser-only read explicit | Second paint or possible flash |
| CSS media query | Responsive presentation | Same DOM, no JavaScript race | Cannot replace behavior that truly needs a browser API |
useId | Labels, descriptions, local accessibility IDs | Stable SSR-safe identifiers | Not for list keys or database IDs |
dynamic(..., { ssr: false }) | Maps, editors, device SDKs that cannot render on server | Avoids SSR for a truly browser-only island | Less initial HTML; worse loading and SEO if overused |
suppressHydrationWarning | One unavoidable text or attribute leaf | Silences a known narrow warning | One level only; does not repair mismatched text |
When client-only dynamic import is appropriate
Some libraries require a real browser, WebGL, a browser extension API, or a
third-party widget that touches document at module load. Isolate that widget;
do not turn an entire route into a client-only page.
"use client"
import dynamic from "next/dynamic"
const LiveMap = dynamic(() => import("./live-map"), {
ssr: false,
loading: () => <p aria-live="polite">Loading map…</p>,
})
export function StoreMap() {
return <LiveMap />
}In the App Router, put this pattern in a Client Component. Keep the surrounding
address, description, and primary content server-rendered. Next.js documents
the ssr: false option in its
lazy-loading guide; it is a
rendering tradeoff, not a general hydration fix.
When suppressHydrationWarning is defensible
For a single inherently local leaf, such as a timestamp where an ISO fallback is unacceptable, this can be a deliberate exception:
"use client"
type LocalClockProps = {
iso: string
}
export function LocalClock({ iso }: LocalClockProps) {
return (
<time dateTime={iso} suppressHydrationWarning>
{new Date(iso).toLocaleString()}
</time>
)
}Use it only after you have confirmed that the surrounding tree and semantics are identical. React limits suppression to one level and does not try to patch the mismatched text. It is unsuitable for layout, navigation, forms, auth state, or anything users can interact with before a later update.
Debugging Matrix for Real Deployments
| Environment | Mismatch you may only see there | What to verify |
|---|---|---|
| Linux or Docker | Container timezone, locale, ICU data, environment flags | Pin server rendering to stable data; test UTC and a visitor timezone |
| macOS or Windows | Developer locale hides a time or number formatting issue | Use explicit locale and timezone in tests when output must be fixed |
| WSL | Different Node binary, environment variables, or filesystem case behavior | Reproduce with the exact Node version and production build command |
| CI/CD | Build-time data or a feature flag changes between build and request | Keep dynamic request data out of statically baked markup unless intended |
| CDN / edge | HTML minification, injected scripts, response rewriting | Disable HTML rewriting for the route and compare origin versus edge response |
| iOS Safari | Automatic phone, date, email, or address linkification | Add the documented format-detection meta tag when linkification changes text |
| CSS-in-JS | Server and client class hashes or style insertion order disagree | Use the framework integration for the exact library and router |
For iOS linkification, add the tag in the document head when your content must not be rewritten into links:
import type { ReactNode } from "react"
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<meta
name="format-detection"
content="telephone=no, date=no, email=no, address=no"
/>
</head>
<body>{children}</body>
</html>
)
}For CSS-in-JS, do not hand-copy a fix for a different library. Styled Components, Emotion, and other systems have different SSR registries and compiler settings. Use the official Next.js CSS-in-JS guidance for your library and router.
Verification Steps
Use this checklist before closing a hydration bug:
- Run
npm run buildfollowed bynpm run start; do not verify only with the development server. - Open the route in a private window with all extensions disabled.
- Reload with the DevTools console open. There should be no hydration warning or recoverable hydration error.
- Compare View Source to Elements near the affected component. Their initial node hierarchy and meaningful attributes should agree.
- Test a saved browser preference, a different locale, and a different timezone when the component formats data.
- Test the deployed CDN path, not only the origin or localhost route.
- Exercise the affected control after hydration. A warning-free page that loses input state or has a broken label relationship is not fixed.
For high-value routes, make hydration warnings a browser-test failure. This complete Playwright example captures console messages for one route:
import { expect, test } from "@playwright/test"
test("account page hydrates without a mismatch", async ({ page }) => {
const hydrationMessages: string[] = []
page.on("console", (message) => {
const text = message.text()
if (
/hydration|initial UI does not match|text content did not match/i.test(
text
)
) {
hydrationMessages.push(text)
}
})
await page.goto("/account", { waitUntil: "networkidle" })
expect(hydrationMessages).toEqual([])
await expect(
page.getByRole("heading", { name: "Your account" })
).toBeVisible()
})Run the test against a production build in CI. If a browser extension is part of your developer setup, it will not be present in the test browser, which helps distinguish an application error from local DOM injection.
Prevention
Render policy checklist
- Treat a component render as a pure function of stable props until hydration completes.
- Keep current time, random generation, and browser reads out of rendered JSX.
- Serialize request-derived values once on the server and use them as initial client props.
- Prefer CSS over
window.innerWidthormatchMediafor responsive layout. - Validate semantic HTML, especially around paragraphs, lists, tables, forms, anchors, and buttons.
- Use
useIdforid,htmlFor,aria-describedby, and similar local relationships. - Configure a compatible server and browser setup for each CSS-in-JS library.
- Review CDN auto-minify, HTML rewrite, translation, and injection features before enabling them globally.
Performance and product tradeoffs
Deferring browser-only work to an effect is usually the right correctness choice, but it adds a second render. Keep that deferred surface small. A locale-formatted timestamp is inexpensive; deferring an entire product page causes a visible layout change and weakens the reason to use SSR in the first place.
Likewise, ssr: false avoids a mismatch by opting out of server rendering for
a component. It is a good boundary for a map or chart that cannot render on
the server. It is a poor default for navigation, product content, or forms
that should be visible, indexable, and usable as soon as HTML arrives.
Common Mistakes
| Mistake | Why it fails | Better move |
|---|---|---|
Adding typeof window directly to JSX branching | Server and browser can select different branches | Render one fallback and change state in useEffect |
Deleting the warning with a root suppressHydrationWarning | It hides evidence while children can still differ | Identify the exact leaf and repair the source |
Turning every component into ssr: false | Removes useful SSR and can harm loading behavior | Isolate only a browser-only dependency |
| Setting a fixed server timezone to match one developer | Visitors still have different local zones | Render stable data first, then localize after hydration |
| Using a global incrementing ID counter | Request order and hydration order can differ | Use useId or a persisted data ID |
| Assuming production is only minified dev | CDN, edge, locale, cookies, and CSS pipeline can differ | Test a production build and deployed response |
Related Guides
- React 19 performance and rendering updates
- debouncing React input without unnecessary requests
- building a controlled two-way binding hook in React
- fixing an occupied Next.js or Node development port
- resolving npm dependency-tree conflicts
- serializing dates safely at an API boundary
Official References
- React:
hydrateRoot - React:
useId - Next.js: React hydration error
- Next.js: lazy loading and skipping SSR
- Next.js: CSS-in-JS
FAQs
How do I fix Hydration failed because the initial UI does not match in Next.js?
Make the server HTML match the component's first browser render. Remove Date,
Math.random, localStorage, window, matchMedia, and locale-dependent
output from render, or pass a stable server value. Read browser-only data in
useEffect after hydration. Also repair invalid HTML nesting and test without
browser extensions or CDN HTML transforms.
Why does a use client component still cause a Next.js hydration error?
"use client" enables client-side interactivity, but Next.js can still render
the component's initial HTML on the server. The first browser render must
produce the same output. Reading storage or window during render can give
the server a fallback and the browser a different value, causing a mismatch.
Does suppressHydrationWarning fix a hydration mismatch?
No. It suppresses React's warning for one element level when a small text or attribute difference is unavoidable, such as a local timestamp. It does not repair a mismatched tree, and React does not patch mismatched text there. Do not use it to hide auth state, invalid markup, navigation, or form differences.
Can a browser extension cause a React hydration error?
Yes. Password managers, grammar tools, translators, dark-mode tools, and ad blockers can add attributes or nodes before React hydrates. Reproduce in a clean browser profile or private window with extensions disabled. If the error disappears, keep the app markup valid and avoid treating extension-only DOM changes as an application bug.
Why does a hydration error happen only in production?
Production may use different request data, locale, timezone, CDN or edge
transforms, CSS extraction, environment variables, and minification than local
development. Run next build and next start, compare the raw response with
the parsed browser DOM, and test the deployed URL with extensions disabled.
Should I disable SSR to get rid of a hydration error?
Only for a genuinely browser-only island, such as a map, editor, or device SDK that cannot render on the server. Disabling SSR avoids the contract by removing the server markup, but it also removes useful initial HTML and can worsen loading, SEO, and perceived performance. Prefer a deterministic first render for ordinary content and controls.
Key takeaways
- •Hydration is a contract: React expects the initial browser render to match the server HTML, not merely look similar.
- •A Next.js Client Component still participates in the initial server render, so useEffect—not a typeof window branch in render—should read browser-only state.
- •Dates, locale formatting, random IDs, localStorage, matchMedia, invalid nesting, browser extensions, and CDN rewrites are common mismatch sources.
- •Compare View Source with the browser's parsed Elements DOM to separate application output from parser or external mutations.
- •Use useId for accessibility IDs and stable server props for request-specific data; do not use Math.random for rendered IDs.
- •suppressHydrationWarning is a one-level escape hatch for an unavoidable leaf mismatch. It does not repair the DOM or make a broken tree safe.
Frequently asked questions
How do I fix Hydration failed because the initial UI does not match in Next.js?
Make the HTML emitted by the server match the component's first browser render. Remove Date, Math.random, localStorage, window, matchMedia, and locale-dependent output from render, or pass a stable server value. Read browser-only data in useEffect after hydration. Also repair invalid HTML nesting and test without browser extensions or CDN HTML transforms.
Why does a use client component still cause a Next.js hydration error?
use client marks a component as interactive and allows hooks, but Next.js can still render its initial HTML on the server. Its first browser render must therefore produce the same output. Reading localStorage or window during render can give the server a fallback and the browser a different value, which creates a mismatch.
Does suppressHydrationWarning fix a hydration mismatch?
No. It suppresses React's warning for one element level when a small text or attribute difference is unavoidable, such as a local timestamp. React documents it as an escape hatch and does not attempt to patch mismatched text there. Do not use it to hide invalid nesting, different component trees, authentication state, or broad document changes.
Can a browser extension cause a React hydration error?
Yes. Password managers, grammar tools, translators, dark-mode tools, and ad blockers can add attributes or nodes before React hydrates. Reproduce in a clean browser profile or private window with extensions disabled. If the error disappears, keep application markup correct and avoid treating extension-only attributes as an application mismatch.
Why does a hydration error happen only in production?
Production can use different request data, time zones, locales, CDN or edge transforms, CSS extraction, environment variables, and minification than local development. Reproduce with next build and next start, then compare the raw response with the parsed browser DOM. Test the deployed path without extensions and inspect any HTML-rewriting CDN feature.