React Error #425: Text content does not match server-rendered HTML
Text content does not match server-rendered HTML
Verified against React 19 source (ReactDOMHostConfig.js), React docs: link-a-react-component-errors, Next.js 16 docs: react-hydration-error · Updated April 2026
> quick_fix
A text node rendered on the server does not match the same text node on the first client render. Move the dynamic value (timestamp, locale string, random ID) into a useEffect or useState that runs only on the client, or wrap the element with suppressHydrationWarning if the divergence is intentional.
// Mismatches every reload
function Greet() {
return <p>Hello at {new Date().toLocaleString()}</p>
}
// Fix - render after mount
function Greet() {
const [t, setT] = useState('')
useEffect(() => setT(new Date().toLocaleString()), [])
return <p suppressHydrationWarning>Hello at {t}</p>
}What causes this error
React's hydration walks the existing DOM and compares each text node to what the client render produces. When a text node differs (even by a single character), invariant #425 fires, the subtree is discarded, and React falls back to a client-only re-render. The server emitted one string, the client computed another, and React refuses to silently choose between them.
How to fix it
- 01
step 1
Read the console diff
React logs the expected vs received text. Search your codebase for either string to find the component.
- 02
step 2
Identify the non-deterministic source
Common offenders: new Date(), Math.random(), Intl.DateTimeFormat without a fixed locale, navigator.language, window.innerWidth, or any read from localStorage.
- 03
step 3
Move the read into useEffect
useEffect runs only on the client after hydration, so reads from window or Date happen post-mount and don't conflict with the server HTML.
useEffect(() => { setLocalised(new Intl.DateTimeFormat(navigator.language).format(date)) }, [date]) - 04
step 4
For intentional divergence, suppressHydrationWarning
Apply to the single element whose text legitimately differs - never on a parent. React will skip the diff for that one node only.
Why #425 happens at the runtime level
During hydration, the React renderer walks server-emitted text nodes and matches them to client-side render output via prepareToHydrateHostTextInstance in ReactFiberCompleteWork.js. The comparison is byte-exact: a different timezone, a different locale collation, or a single character of whitespace triggers the mismatch path. React then calls didNotMatchHydratedTextInstance, falls back to client rendering for that node, and logs invariant #425. The check exists because silently accepting the client value would corrupt the SSR contract that the streamed HTML is the source of truth.
Common debug mistakes for #425
- Using toLocaleString() on the server expecting it to use the user's locale - server has its own default locale (often en-US) and the client uses navigator.language, guaranteeing divergence.
- Calling Math.random() inside useMemo with empty deps - useMemo runs on both server and client with different RNG states, the cached value differs, and #425 fires.
- Reading from a context that has different default values on server vs client without explicit hydration sync - the context state diverges and any text derived from it mismatches.
- Trying to fix it with key={Date.now()} - this forces a full remount on every render, masks #425 with infinite re-renders, and tanks performance.
- Using the unsafe HTML-injection prop to bypass the diff - the diff still runs at the parent level and the warning becomes harder to trace.
When #425 signals a deeper problem
Recurring #425 usually means the SSR contract is not enforced at the type level. When the same component reads from server-time clocks, browser locale APIs, and random number generators in a single render path, every dynamic field becomes a hydration risk. The architectural fix is to mark genuinely dynamic content with 'use client' and hydrate it from a known server snapshot passed as props, or render it inside a useEffect-gated child. Next.js 16's 'use cache' directive solves this for cacheable values. Without that boundary, every new feature touching the DOM, locale, or randomness ships another #425.
Editor's take
This error has a particular way of surfacing at the worst moment: the site is getting crawled by Google for the first time after a Next.js 14+ App Router migration, and hydration mismatches are silently corrupting the initial HTML before React can recover. A small startup with one frontend engineer on call discovers it not through an error boundary but through a blank flash on mobile — Lighthouse flags it, the CEO sees it on their phone during a demo, and the team spends three hours bisecting commits before tracing it back to a `new Date().toLocaleDateString()` call sitting directly in JSX without `suppressHydrationWarning` or a `useEffect` guard. The monolith gets hit harder here than a microservices setup because there's no staging environment with realistic locale settings to catch it first.
Finding and fixing #425 cleanly is a mid-career signal. A junior dev hits it, panics, and reaches for `suppressHydrationWarning` on everything — which papers over the symptom without understanding the SSR contract. The engineer who genuinely understands it has internalized that the server and client are two separate runtimes and that any value derived from `window`, `navigator.language`, `crypto.randomUUID()`, or `Date.now()` is server-unsafe by definition. That insight transfers immediately to understanding streaming, Suspense boundaries, and why `use client` does not mean "skip SSR" in Next.js App Router.
When you're tracing #425, you rarely find it alone. React Error #418 ("Hydration failed because the initial UI does not match") almost always appears in the same console session — #418 is the component-tree mismatch; #425 is the text-node variant of the same contract violation. Upstream you'll often find `NEXT_PUBLIC_` env vars containing locale or timezone strings being read at module initialization rather than inside components, and downstream you'll hit React Error #423 ("There was an error while hydrating"), which is the fallback that fires when React gives up recovering the tree entirely.
By Bikram Nath · Curator · Updated April 2026
Frequently asked questions
Is #425 the same as #418?
Closely related. #418 covers any hydration mismatch (text, attribute, structure). #425 specifically covers text-content divergence, which is the most common subset.
Does suppressHydrationWarning hide all errors?
No. It suppresses the diff for one element's immediate text content. Children, attributes, and structural diffs still throw.
How do I detect #425 before deploying?
Run a production build with hydration markers enabled (Next.js does this by default) and crawl the site with the React DevTools open. The console logs every mismatch on first paint.