# Next.js hydration errors

> A Next.js hydration error means the server HTML and the browser's first render differ. The messages, common causes, fixes, and how to test every route.

Source: https://hydration.jscrate.dev/docs/frameworks/nextjs
Last updated: 2026-09-18

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](https://nextjs.org/docs/messages/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:

```text
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](https://hydration.jscrate.dev/docs/errors/hydration-failed-server-rendered-html-didnt-match-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:

```text
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](https://hydration.jscrate.dev/docs/compare/nextjs-dev-overlay)). Production builds
print a minified code such as
[Minified React error #418](https://hydration.jscrate.dev/docs/errors/minified-react-error-418) instead of
the message, and some mismatches only happen there. See
[hydration errors only in production](https://hydration.jscrate.dev/docs/guides/hydration-error-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](https://hydration.jscrate.dev/docs/causes/time)                       |
| Timezone and locale | `toLocaleString()`, `Intl` formatters without a `timeZone` or locale | [Timezone](https://hydration.jscrate.dev/docs/causes/timezone), [locale](https://hydration.jscrate.dev/docs/causes/locale) |
| Browser-only APIs   | `typeof window !== "undefined"`, `window`, `navigator` in render     | [Browser APIs](https://hydration.jscrate.dev/docs/causes/browser-api)                         |
| Storage             | `localStorage` read during render or in initial state                | [localStorage](https://hydration.jscrate.dev/docs/causes/storage)                             |
| Theme               | dark mode read on the client, `next-themes`                          | [Theme](https://hydration.jscrate.dev/docs/causes/theme)                                      |
| Invalid HTML        | `<div>` inside `<p>`, `<a>` inside `<a>`                             | [Invalid nesting](https://hydration.jscrate.dev/docs/causes/invalid-html)                     |
| Browser extensions  | attributes added by password managers and translators                | [Extensions](https://hydration.jscrate.dev/docs/causes/extension)                             |
| CSS-in-JS           | styled-components without its style registry                         | [CSS-in-JS](https://hydration.jscrate.dev/docs/causes/css-in-js)                              |
| Scripts             | a script that changes the page before React hydrates it              | [Third-party scripts](https://hydration.jscrate.dev/docs/causes/third-party-script)           |
| CDN                 | Cloudflare Auto Minify rewriting the HTML                            | [CDN rewrites](https://hydration.jscrate.dev/docs/causes/cdn)                                 |

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](https://nextjs.org/docs/app/getting-started/server-and-client-components),
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:

```tsx title="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](https://hydration.jscrate.dev/docs/guides/useeffect-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:

```tsx title="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](https://hydration.jscrate.dev/docs/guides/next-dynamic-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](https://hydration.jscrate.dev/docs/guides/suppresshydrationwarning).

## 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](https://hydration.jscrate.dev/docs/adapters)
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.

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

```ts title="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:

```ts title="next.config.ts"
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  productionBrowserSourceMaps: true,
};

export default nextConfig;
```

### Catch it in your editor

The [ESLint plugin](https://hydration.jscrate.dev/docs/eslint) finds the same causes as you type. Its `next`
preset skips Server Components, where the clock and `window` checks are safe:

```bash
npm install -D eslint-plugin-hydration-proof
```

```js title="eslint.config.mjs"
import hydrationProof from "eslint-plugin-hydration-proof";

export default [hydrationProof.configs.next];
```

## Related

- [React hydration errors](https://hydration.jscrate.dev/docs/guides/react-hydration-error), outside Next.js
- [Debugging hydration errors](https://hydration.jscrate.dev/docs/guides/debug-hydration-errors) step by step
- [All causes of hydration errors](https://hydration.jscrate.dev/docs/causes)
- [Detect hydration errors in CI](https://hydration.jscrate.dev/docs/ci)
- [Quick start](https://hydration.jscrate.dev/docs/quick-start)
