# Text content does not match server-rendered HTML

> Text content does not match server-rendered HTML is React 18's error (#425) for text that differs between server and browser, usually a time, date or number.

Source: https://hydration.jscrate.dev/docs/errors/text-content-does-not-match-server-rendered-html
Last updated: 2026-09-18

"Text content does not match server-rendered HTML" is the error React 18
throws when a piece of text in the first browser render differs from the
server's HTML. It is usually a time, a formatted date or number, or a random
value. Render the same text on both sides, or set the changing value after
hydration.

## The error

In development, React 18 first warns with both values, then throws:

```text
Warning: Text content did not match. Server: "12:04:31" Client: "12:04:33"
Uncaught Error: Text content does not match server-rendered HTML.
Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
```

A production build prints only the code:

```text
Minified React error #425; visit https://reactjs.org/docs/error-decoder.html?invariant=425 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
```

React 19 no longer has this message. A text difference throws the general
hydration error, #418, with the word "text":

```text
Hydration failed because the server rendered text didn't match the client.
```

See [the React 19 error](https://hydration.jscrate.dev/docs/errors/hydration-failed-server-rendered-html-didnt-match-client)
for its diff.

## What it means

React 18 checks every text node while it hydrates. The warning names the text content that did not match, with the server value first and the client value second. React then gives up on the server HTML for that part of the page and renders it again in the browser, up to the nearest `<Suspense>` boundary or the whole root.

Two details make it harder to track down:

- React 18 logs the warning only for the first mismatch on a page. Fix it,
  reload, and the next one appears.
- The error itself carries no values. In production you get #425 and nothing
  else, so users' consoles do not tell you which text it was.

## Text content does not match server-rendered HTML (Next.js)

Next.js 13 and 14 use React 18, so they show this error in the dev overlay,
often next to "Hydration failed because the initial UI does not match…". The
causes are the same as in any React app. Two details are specific to Next.js:

- **Static pages keep the build's text.** A page pre-rendered at build time
  keeps that moment's date, and every visitor's browser renders a new one.
- **Server Components are not the problem.** Text a Server Component renders
  is never re-rendered in the browser. The mismatch is in a Client Component
  (`"use client"`), which renders on the server and again in the browser.

On Next.js 15 and later you get the React 19 wording instead.

## Common causes

| The text is                               | Why it differs                                                  | Fix guide                                                  |
| ----------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------- |
| A time or "3 minutes ago"                 | The clock moves between the two renders                         | [Time-dependent values](https://hydration.jscrate.dev/docs/causes/time)                 |
| A date or time of day                     | The server formats in UTC, the browser in the user's timezone   | [Timezone](https://hydration.jscrate.dev/docs/causes/timezone)                          |
| A number, price or date format            | `toLocaleString()` uses the server's locale, then the browser's | [Locale](https://hydration.jscrate.dev/docs/causes/locale)                              |
| An id, a shuffled item, a random greeting | `Math.random()` returns a new value                             | [Random values](https://hydration.jscrate.dev/docs/causes/random)                       |
| A name, count or cart total               | Read from `localStorage` or fetched again                       | [Storage](https://hydration.jscrate.dev/docs/causes/storage), [data](https://hydration.jscrate.dev/docs/causes/data) |
| Translated or corrected text              | A browser extension edited it                                   | [Browser extensions](https://hydration.jscrate.dev/docs/causes/extension)               |

## How to fix it

1. **Find the text.** The warning shows both values. In production, run the
   app in development, or use hydration-proof (below).
2. **Pass the value from the server** when both sides can use the same one.
   Before, each render reads its own clock and its own timezone:

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

   export function LastUpdated() {
     return <p>Updated at {new Date().toLocaleTimeString()}</p>;
   }
   ```

   After, the server's timestamp arrives as a prop, and the format names its
   locale and timezone:

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

   export function LastUpdated({ updatedAt }: { updatedAt: number }) {
     const time = new Date(updatedAt).toLocaleTimeString("en-US", {
       timeZone: "UTC",
     });
     return <p>Updated at {time}</p>;
   }
   ```

3. **Set live values after hydration.** A ticking clock or a value only the
   browser knows starts from a placeholder and is filled in by an effect.
   [Render the time after hydration](https://hydration.jscrate.dev/docs/causes/time#render-the-time-after-hydration)
   shows the pattern.
4. **Suppress only what is meant to differ.** For a timestamp whose first
   value does not matter, `suppressHydrationWarning` on that one element keeps
   the server text without an error. Read
   [when suppressHydrationWarning is safe](https://hydration.jscrate.dev/docs/guides/suppresshydrationwarning)
   first.

## Find every instance

hydration-proof compares the text of every element on every route and
reports each difference as [HP1001](https://hydration.jscrate.dev/docs/issues/hp1001), with both values,
the component and the likely cause:

```bash
npx hydration-proof test --probe
```

```text
  ✖ /blog 912ms  1 error
    HP1001 Text differs between server and client  (time-dependent value, 97%)
      p  in LastUpdated
      server: "Updated at 12:04:31"
      client: "Updated at 12:04:33"
      components/last-updated.tsx:4:10
      → Pass the timestamp the server used as a prop, or render the time after mount (useEffect).
```

`--probe` reloads the page with one thing changed at a time (the clock, the
locale, the timezone) to prove the cause. See [probes](https://hydration.jscrate.dev/docs/probes).

The ESLint rules catch the source in the editor:
[`no-date-in-render`](https://hydration.jscrate.dev/docs/rules/no-date-in-render),
[`no-random-in-render`](https://hydration.jscrate.dev/docs/rules/no-random-in-render),
[`no-locale-without-explicit-locale`](https://hydration.jscrate.dev/docs/rules/no-locale-without-explicit-locale)
and [`no-timezone-without-explicit-timezone`](https://hydration.jscrate.dev/docs/rules/no-timezone-without-explicit-timezone).

## Related

- [Minified React error #425](https://hydration.jscrate.dev/docs/errors/minified-react-error-425)
- [Fix date and time hydration errors](https://hydration.jscrate.dev/docs/causes/time)
- [toLocaleString and locale mismatches](https://hydration.jscrate.dev/docs/causes/locale)
- [HP1001: text differs between server and client](https://hydration.jscrate.dev/docs/issues/hp1001)
- [All React hydration error messages](https://hydration.jscrate.dev/docs/errors)
