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.
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.