A Next.js hydration error means the HTML the server sent differs from what React renders in the browser on its first pass. The usual causes are the clock, the locale or timezone, browser-only APIs, invalid HTML nesting and browser extensions. Render the same output on both sides first, then change it in an effect after hydration.
Common hydration errors in Next.js
The Next.js hydration failed message depends on the React version your app runs. Next.js explains all of them on one page of its docs, react-hydration-error.
The Next.js 15 hydration error (React 19)
The App Router in Next.js 15 and 16 uses React 19, which reports a mismatch with one of these messages and a diff of the two renders:
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 what each part of the message means.
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 of these are logged rather than thrown: React 19's attribute message, and
React 18's Warning: Text content did not match lines. A Next.js hydration
warning still means the page differs from the server HTML. React does not patch
attributes during hydration, so the server's value stays on the page.
In development, the Next.js dev overlay shows the error for the page you have open (how it compares). Production builds print a minified code such as Minified React error #418 instead of the message, and some mismatches only happen there. See hydration errors only in production.
What causes a Next.js hydration error?
Every Next.js React hydration error comes from the same place: the server and the browser rendered the same component with different inputs. A Next.js hydration mismatch usually has one of these causes:
| 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 |
The Next.js docs also mention iOS, which turns phone numbers, email addresses
and dates in text into links. 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 prevent a hydration error. It marks where the client
part of your app starts, but Client Components are still rendered to HTML on the
server: Next.js uses them to
prerender the page,
then hydrates them in the browser. Date.now() in a Client Component runs
twice, at two different moments.
Server Components never hydrate. They render once, on the server, and the
browser receives their result without running them again. Time, random values
or window checks in a Server Component cannot cause a mismatch on their own.
In the Pages Router there are no Server Components: every component on a page renders on the server and hydrates in the browser.
How to fix a hydration error in Next.js
How to solve a React hydration error in Next.js depends on the cause, and hydration-proof names it for each finding. These fixes cover most of them, in the order to try them.
Render the same thing first, then update in an effect
Render output that does not depend on the browser, then change it in
useEffect, which only runs in the browser 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>;
}useEffect and two-pass rendering explains the pattern and when the flash of the first value matters.
Skip server rendering with next/dynamic
A component that cannot render on the server (a map, a chart that measures the
window) can skip it. ssr: false only works in Client Components; a Server
Component that uses it fails with an error:
"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 the trade-offs.
Pass server values down as props
When the value comes from the server (the time the page was rendered, the user's locale from a cookie), read it once in a Server Component and pass it to the Client Component. Both renders then use the same value.
Use suppressHydrationWarning sparingly
suppressHydrationWarning on an element tells React to keep the server's text
without reporting it. It only works one level deep, and React does not patch
the text, so the page shows the server value. It suits a timestamp whose first
value does not matter, and the <html> element that next-themes changes
before hydration. Everywhere else it hides a real bug. See
when suppressHydrationWarning is safe.
Test every route with hydration-proof
hydration-proof loads every route of your app in a real browser, compares the
server HTML with the hydrated DOM and reports each difference with its element,
both values, the likely cause and the fix. The next adapter
is picked when package.json has a next dependency or the project has a
next.config.* file:
- 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 tests the development and production builds in one run and
marks findings that appear in only one of them. Development builds give exact
source lines. Production builds show what your users get, including attribute
mismatches React 19 never reports in production.
A config is optional. Add one for the values discovery cannot know:
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]": ["1", "42"],
},
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