Hydration Proof

Search documentation

Find a page or section

App Router and Pages Router, React 18 and React 19.

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:

CauseTypical codeFix
The clocknew Date(), Date.now(), "3 minutes ago"Time-dependent values
Timezone and localetoLocaleString(), Intl formatters without a timeZone or localeTimezone, locale
Browser-only APIstypeof window !== "undefined", window, navigator in renderBrowser APIs
StoragelocalStorage read during render or in initial statelocalStorage
Themedark mode read on the client, next-themesTheme
Invalid HTML<div> inside <p>, <a> inside <a>Invalid nesting
Browser extensionsattributes added by password managers and translatorsExtensions
CSS-in-JSstyled-components without its style registryCSS-in-JS
Scriptsa script that changes the page before React hydrates itThird-party scripts
CDNCloudflare Auto Minify rewriting the HTMLCDN 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:

app/components/greeting.tsx
"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:

app/store/map-client.tsx
"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 your build script when it runs next build), then next start --port {port}. The app is built only when .next/BUILD_ID is missing.
  • Development mode: next dev, opened on localhost because Next.js blocks development resources for other hosts.
  • Routes: discovered from app/ and pages/, 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 (generateStaticParams and getStaticPaths).
  • 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 with router.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:

hydration-proof.config.ts
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:

next.config.ts
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-proof
eslint.config.mjs
import hydrationProof from "eslint-plugin-hydration-proof";
 
export default [hydrationProof.configs.next];