A Next.js hydration error means the first browser render does not match the server HTML. Check dates, locale and timezone formatting, browser-only values, and invalid HTML first. Fix the value that differs so both sides render the same thing before any effects run.
Common hydration errors in Next.js
The Next.js hydration failed message depends on your React version. See the Next.js hydration error reference for its explanation of the common cases.
The Next.js 15 hydration error (React 19)
With React 19, you may see one of these messages and a diff:
Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.
Hydration failed because the server rendered text didn't match the client.
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up.See server rendered HTML didn't match the client for help reading the diff.
Next.js 13 and 14 (React 18)
With React 18, the same bug shows up as:
Hydration failed because the initial UI does not match what was rendered on the server.
Text content does not match server-rendered HTML.
There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.Warnings and production builds
Some differences show as warnings rather than errors. A Next.js hydration warning still points to a real difference. React may leave the server's attribute value in place, so check the rendered page as well as the console.
In development, the Next.js overlay shows the error on the page you have open (comparison). Production may show only a code such as #418. If the error appears only after deployment, follow the production guide.
What causes a Next.js hydration error?
A Next.js React hydration error often starts with one of these inputs. A Next.js hydration mismatch is easier to fix once you identify the changed value:
| Cause | Typical code | Fix |
|---|---|---|
| The clock | new Date(), Date.now(), "3 minutes ago" | Time-dependent values |
| Timezone and locale | toLocaleString(), Intl formatters without a timeZone or locale | Timezone, locale |
| Browser-only APIs | typeof window !== "undefined", window, navigator in render | Browser APIs |
| Storage | localStorage read during render or in initial state | localStorage |
| Theme | dark mode read on the client, next-themes | Theme |
| Invalid HTML | <div> inside <p>, <a> inside <a> | Invalid nesting |
| Browser extensions | attributes added by password managers and translators | Extensions |
| CSS-in-JS | styled-components without its style registry | CSS-in-JS |
| Scripts | a script that changes the page before React hydrates it | Third-party scripts |
| CDN | Cloudflare Auto Minify rewriting the HTML | CDN rewrites |
On iOS, automatic link detection can change phone numbers, addresses, and dates
before hydration. A
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
tag turns that off.
Why does a Next.js "use client" hydration error happen?
"use client" does not mean "browser only." Next.js can still
prerender a Client Component
on the server. The browser then renders it again during hydration. A call to
Date.now() in render can return a different value each time.
Server Component code does not run in the browser. Its output can still contain invalid HTML, but its own time or random calculation is not repeated during hydration.
In the Pages Router, page components render on the server and hydrate in the browser.
How to fix a hydration error in Next.js
How to solve a React hydration error in Next.js depends on what changed. Start with the fix that keeps useful content in the server HTML.
Render the same thing first, then update in an effect
When the value exists only in the browser, render a stable initial value. Read
the browser in useEffect after hydration:
"use client";
import { useEffect, useState } from "react";
export function Greeting() {
const [name, setName] = useState<string | null>(null);
useEffect(() => {
setName(localStorage.getItem("name"));
}, []);
return <p>Hello{name ? `, ${name}` : ""}</p>;
}See two-pass rendering for the tradeoff: users may briefly see the initial value.
Skip server rendering with next/dynamic
If a component cannot run on the server, load it with next/dynamic and
ssr: false. In the App Router, keep this call inside a Client Component:
"use client";
import dynamic from "next/dynamic";
const StoreMap = dynamic(() => import("./store-map"), {
ssr: false,
loading: () => <p>Loading map…</p>,
});
export function MapClient() {
return <StoreMap />;
}See next/dynamic with ssr: false for when to use it.
Pass server values down as props
If the server knows the value, such as a locale stored in a cookie, read it there and pass it to the Client Component. Both renders then start with the same value.
Use suppressHydrationWarning sparingly
suppressHydrationWarning hides an expected difference on one element. It
does not make the renders match. Use it for a value that cannot reasonably be
the same, then check its limits.
Test every route with hydration-proof
hydration-proof tests routes in a browser and shows the changed element, both
values, and a likely cause. It selects the next adapter
when it finds a next dependency or next.config.*:
- Build and start:
next build(or yourbuildscript when it runsnext build), thennext start --port {port}. The app is built only when.next/BUILD_IDis missing. - Development mode:
next dev, opened onlocalhostbecause Next.js blocks development resources for other hosts. - Routes: discovered from
app/andpages/, with route groups, parallel routes and private folders handled and API routes skipped. Dynamic routes get up to three example values from the pages the build pre-rendered (generateStaticParamsandgetStaticPaths). - Not-found page: a URL that does not exist is loaded too, to check that the not-found page hydrates.
- Navigation: with
--navigation, client-side navigation withrouter.push(App Router and Pages Router) is compared with loading each route directly.
npm install -D hydration-proof
npx hydration-proof install
npx hydration-proof test --mode both--mode both checks development and production. Use it when a finding appears
in only one mode or when you need development messages alongside production
behavior.
A config is optional. Add dynamic route values when discovery cannot find them:
import { defineConfig } from "hydration-proof";
export default defineConfig({
routes: {
// One real value per dynamic segment, or the route is skipped.
dynamic: {
"/blog/[slug]": ["hello-world"],
"/products/[id]": ["your-stable-test-product-id"],
},
exclude: ["/api/**", "/admin/**"],
},
});For file and line numbers in production builds, turn on browser source maps:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
productionBrowserSourceMaps: true,
};
export default nextConfig;Catch it in your editor
The ESLint plugin finds the same causes as you type. Its next
preset skips Server Components, where the clock and window checks are safe:
npm install -D eslint-plugin-hydration-proofimport hydrationProof from "eslint-plugin-hydration-proof";
export default [hydrationProof.configs.next];Related
- React hydration errors, outside Next.js
- Debugging hydration errors step by step
- All causes of hydration errors
- Detect hydration errors in CI
- Quick start