"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:
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:
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":
Hydration failed because the server rendered text didn't match the client.See the React 19 error 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 |
| A date or time of day | The server formats in UTC, the browser in the user's timezone | Timezone |
| A number, price or date format | toLocaleString() uses the server's locale, then the browser's | Locale |
| An id, a shuffled item, a random greeting | Math.random() returns a new value | Random values |
| A name, count or cart total | Read from localStorage or fetched again | Storage, data |
| Translated or corrected text | A browser extension edited it | Browser extensions |
How to fix it
-
Find the text. The warning shows both values. In production, run the app in development, or use hydration-proof (below).
-
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:
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:
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>; } -
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 shows the pattern.
-
Suppress only what is meant to differ. For a timestamp whose first value does not matter,
suppressHydrationWarningon that one element keeps the server text without an error. Read when suppressHydrationWarning is safe first.
Find every instance
hydration-proof compares the text of every element on every route and reports each difference as HP1001, with both values, the component and the likely cause:
npx hydration-proof test --probe ✖ /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.
The ESLint rules catch the source in the editor:
no-date-in-render,
no-random-in-render,
no-locale-without-explicit-locale
and no-timezone-without-explicit-timezone.