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 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:

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

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:

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>;
}

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:

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 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 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 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:

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]": ["1", "42"],
    },
    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];