# React hydration errors

> A React hydration error means the server HTML and the first client render differ. The messages in React 18 and 19, what causes each one, and how to fix it.

Source: https://hydration.jscrate.dev/docs/guides/react-hydration-error
Last updated: 2026-09-18

A React hydration error means the HTML the server sent and the first render in the browser are different. React logs an error and, in React 19, throws that part of the server HTML away and renders it again on the client. The fix is to make both renders produce the same output.

## Hydration error meaning: what React is telling you

React renders your components twice: once on the server to produce HTML, and once in the browser during [hydration](https://hydration.jscrate.dev/docs/guides/what-is-hydration), where it expects the exact same output. A hydration error in React JS apps is the report that the two did not match.

A React hydration mismatch comes in three kinds:

- **Text:** the server wrote "5:00 AM", the browser rendered "10:00 AM".
- **Attributes:** a `className`, `style`, `href` or `data-*` value differs.
- **Structure:** one side rendered an element the other did not, or a different one.

Every hydration problem in React comes from one of two places: code that renders different output on the two sides, or something that changed the HTML between the server and React (the browser's parser, an extension, a script or a CDN).

## The messages you see

The wording depends on the React version and on the build. These are the ones people search for:

```text
Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on 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.
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.
In HTML, <div> cannot be a descendant of <p>. This will cause a hydration error.
Minified React error #418; visit https://react.dev/errors/418
```

| Message                                                                   | Where you see it                                       | Decoded                                                                       |
| ------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------- |
| Hydration failed because the server rendered HTML didn't match the client | React 19 (#418 in production)                          | [Page](https://hydration.jscrate.dev/docs/errors/hydration-failed-server-rendered-html-didnt-match-client) |
| A tree hydrated but some attributes … didn't match                        | React 19, development only                             | [Page](https://hydration.jscrate.dev/docs/errors/tree-hydrated-but-attributes-didnt-match)                 |
| Hydration failed because the initial UI does not match                    | React 18                                               | [Page](https://hydration.jscrate.dev/docs/errors/hydration-failed-initial-ui-does-not-match)               |
| Text content does not match server-rendered HTML                          | React 18                                               | [Page](https://hydration.jscrate.dev/docs/errors/text-content-does-not-match-server-rendered-html)         |
| There was an error while hydrating                                        | React 18                                               | [Page](https://hydration.jscrate.dev/docs/errors/there-was-an-error-while-hydrating)                       |
| Prop `className` did not match                                            | React 18, development                                  | [Page](https://hydration.jscrate.dev/docs/errors/prop-classname-did-not-match)                             |
| Expected server HTML to contain a matching …                              | React 18, development                                  | [Page](https://hydration.jscrate.dev/docs/errors/expected-server-html-to-contain-a-matching)               |
| Extra attributes from the server                                          | React 18, development                                  | [Page](https://hydration.jscrate.dev/docs/errors/extra-attributes-from-the-server)                         |
| `<div>` cannot be a descendant of `<p>`                                   | React 19, development (React 18: `validateDOMNesting`) | [Page](https://hydration.jscrate.dev/docs/errors/div-cannot-be-a-descendant-of-p)                          |
| Minified React error #418, #423, #425                                     | Production builds                                      | [Page](https://hydration.jscrate.dev/docs/errors/minified-react-error-codes)                               |

The full list, with the React 18 and 19 wording side by side, is on [React hydration error messages](https://hydration.jscrate.dev/docs/errors).

## What causes a hydration error in React?

Each cause below has its own fix guide. hydration-proof names the likely cause for every finding.

| Cause               | Typical code                                               | Fix guide                                              |
| ------------------- | ---------------------------------------------------------- | ------------------------------------------------------ |
| The clock           | `Date.now()`, `new Date()` in render                       | [Time](https://hydration.jscrate.dev/docs/causes/time)                              |
| Timezone            | `toLocaleTimeString()` without `timeZone`                  | [Timezone](https://hydration.jscrate.dev/docs/causes/timezone)                      |
| Locale              | `toLocaleString()`, `Intl.NumberFormat()` without a locale | [Locale](https://hydration.jscrate.dev/docs/causes/locale)                          |
| Random values       | `Math.random()`, `crypto.randomUUID()`                     | [Random](https://hydration.jscrate.dev/docs/causes/random)                          |
| Browser-only APIs   | `typeof window !== "undefined"`, `navigator`               | [Browser APIs](https://hydration.jscrate.dev/docs/causes/browser-api)               |
| Storage             | `localStorage.getItem()` in render or initial state        | [Storage](https://hydration.jscrate.dev/docs/causes/storage)                        |
| Screen size         | `matchMedia()`, `window.innerWidth`                        | [Media query](https://hydration.jscrate.dev/docs/causes/media-query)                |
| Theme               | dark mode read from storage                                | [Theme](https://hydration.jscrate.dev/docs/causes/theme)                            |
| Data                | the client fetches again and gets a different answer       | [Data](https://hydration.jscrate.dev/docs/causes/data)                              |
| Invalid HTML        | `<div>` in `<p>`, `<a>` in `<a>`, `<tr>` in `<table>`      | [Invalid HTML](https://hydration.jscrate.dev/docs/causes/invalid-html)              |
| CSS-in-JS           | class names generated in a different order                 | [CSS-in-JS](https://hydration.jscrate.dev/docs/causes/css-in-js)                    |
| Ids                 | counters or random ids instead of `useId()`                | [Unstable ids](https://hydration.jscrate.dev/docs/causes/unstable-id)               |
| Browser extensions  | attributes such as `cz-shortcut-listen`                    | [Extensions](https://hydration.jscrate.dev/docs/causes/extension)                   |
| Scripts             | a tag manager edits the DOM before React                   | [Third-party scripts](https://hydration.jscrate.dev/docs/causes/third-party-script) |
| CDN rewrites        | HTML minification at the edge                              | [CDN](https://hydration.jscrate.dev/docs/causes/cdn)                                |
| Server Action forms | `useActionState` with a permalink                          | [Form state](https://hydration.jscrate.dev/docs/causes/form-state)                  |

## How to fix a React hydration error

1. **Find the element.** Read the diff React logs in development, or let a tool point at it. [Debugging hydration errors](https://hydration.jscrate.dev/docs/guides/debug-hydration-errors) covers both.
2. **Name the cause.** Compare the two values: a timestamp, a number in another format, a random token or a different class name each point at one row of the table above.
3. **Make the first client render match the server.** Pick the first fix that applies:
   - pass the value the server used as a prop, so both sides render the same data;
   - move browser-only reads into `useEffect`, so they happen after hydration ([two-pass rendering](https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering));
   - read external stores with `useSyncExternalStore` and a `getServerSnapshot` that returns the server's value;
   - render the component only in the browser ([client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component));
   - fix invalid nesting so the browser does not rewrite the HTML.
4. **Check every page again,** not only the one you fixed.

A typical fix, for a date formatted in the reader's timezone:

```tsx title="app/components/last-login.tsx"
"use client";

// Before: the server formats in its timezone, the browser in the reader's.
export function LastLogin({ at }: { at: number }) {
  return <p>Signed in at {new Date(at).toLocaleTimeString()}</p>;
}
```

```tsx title="app/components/last-login.tsx"
"use client";

// After: the same locale and timezone on both sides.
export function LastLogin({ at, timeZone }: { at: number; timeZone: string }) {
  return (
    <p>Signed in at {new Date(at).toLocaleTimeString("en-US", { timeZone })}</p>
  );
}
```

## How to solve a hydration error without hiding it

`suppressHydrationWarning` makes React stop reporting a difference in one element's own text and attributes. React keeps the server's value and does not patch it, so the user sees the stale value. It is for values that can never match, such as a live clock, not for bugs. [suppressHydrationWarning](https://hydration.jscrate.dev/docs/guides/suppresshydrationwarning) explains when it is safe and why it often does nothing.

## Which React hydration issues stay silent?

Not every hydration mismatch error throws:

- **React 19 production builds do not report attribute mismatches at all.** A wrong `className`, `style` or `href` stays on the page with no error in the console.
- **Production builds minify messages.** You get "Minified React error #418" with no element and no diff. See [errors only in production](https://hydration.jscrate.dev/docs/guides/hydration-error-only-in-production).
- **A hydration mismatch warning is a development feature.** React 18's "Warning: Prop `className` did not match" and React 19's attribute warning are never printed by production builds.

These are the ones a hydration warning in your console will never show you, and why hydration-proof compares every attribute with what React renders on the client.

## Framer Motion hydration error

Motion (formerly Framer Motion) renders the `initial` state of a `motion` component into the server HTML. Its docs say motion components "are fully compatible with server-side rendering, meaning the initial state of the component will be reflected in the server-generated output" ([motion.dev](https://motion.dev/docs/react-motion-component)).

So a framer motion hydration error appears when `initial` or `animate` depends on something only the browser knows: the window size, a random value, or a stored preference. The server writes one inline `style`, the browser computes another.

```tsx title="components/slide-in.tsx"
"use client";

import type { ReactNode } from "react";
import { motion } from "motion/react";

// Before: window.innerWidth is unknown on the server.
export function SlideIn({ children }: { children: ReactNode }) {
  const from = typeof window === "undefined" ? 0 : -window.innerWidth;
  return (
    <motion.div initial={{ x: from }} animate={{ x: 0 }}>
      {children}
    </motion.div>
  );
}
```

```tsx title="components/slide-in.tsx"
"use client";

import type { ReactNode } from "react";
import { motion } from "motion/react";

// After: the same starting value on both sides, in a unit that does not
// depend on the screen.
export function SlideIn({ children }: { children: ReactNode }) {
  return (
    <motion.div initial={{ x: "-100%" }} animate={{ x: 0 }}>
      {children}
    </motion.div>
  );
}
```

To skip the enter animation entirely, `initial={false}` renders the `animate` values on the server and in the browser. A style difference is an attribute mismatch, so React 19 production builds report nothing; hydration-proof reports it as [HP1003](https://hydration.jscrate.dev/docs/issues/hp1003).

## Tools that find hydration errors

| Tool                                                    | When it runs                                       | What it finds                                                           |
| ------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------- |
| React's console messages                                | Development, the page you have open                | The mismatches React reports, with a diff in React 19                   |
| [Next.js dev overlay](https://hydration.jscrate.dev/docs/compare/nextjs-dev-overlay) | `next dev`, one page at a time                     | The same, with a code frame                                             |
| [eslint-plugin-hydration-proof](https://hydration.jscrate.dev/docs/eslint)           | Your editor and CI, on source code                 | Patterns that cause mismatches, before they run                         |
| [hydration-proof test](https://hydration.jscrate.dev/docs/quick-start)               | CI, every route, development and production builds | Mismatches that happen, including silent attribute ones, with the cause |
| [Sentry](https://hydration.jscrate.dev/docs/compare/sentry-hydration-errors)         | Production, real users                             | Errors React reports in your users' browsers                            |

```bash
npx hydration-proof test --mode both
```

## Related

- [What is hydration in React?](https://hydration.jscrate.dev/docs/guides/what-is-hydration)
- [Debugging hydration errors](https://hydration.jscrate.dev/docs/guides/debug-hydration-errors)
- [Next.js hydration errors](https://hydration.jscrate.dev/docs/frameworks/nextjs)
- [Common causes of hydration errors](https://hydration.jscrate.dev/docs/causes)
- [The ESLint rules that catch them early](https://hydration.jscrate.dev/docs/rules)
