Home › JavaScript › Text Content Does Not Match: Fix Hydration Mismatch
Intermediate 5 min · September 23, 2026

Text Content Does Not Match: Fix Hydration Mismatch

Render deterministic HTML on both passes and defer client values to useEffect — mismatched dates, randoms, and window reads break hydration..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 12 min
  • ✓A Next.js app with server rendering
  • ✓Basic React hooks knowledge
  • ✓Familiarity with browser DevTools
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • The server and client rendered different text, so React discarded the server HTML. Find the differing node in the error's expected-versus-actual quote.
  • Date.now, Math.random, toLocaleString, and window reads during render guarantee a mismatch. Move them into useEffect and render a placeholder first.
  • Pin locale and timeZone explicitly on every formatter. Server ICU data and browser ICU data rarely agree.
  • Use suppressHydrationWarning only on single elements that must vary, like a live clock — never on whole subtrees.
✦ Definition~90s read
What is Next.js Hydration Mismatch Fix?

A hydration mismatch is React's report that the server's HTML and the client's first render disagree. Next.js server-renders each page to HTML for fast first paint and SEO, then the client builds the same tree in memory and compares. When a text node differs — a clock, a random id, a locale-formatted price — React logs text content does not match and discards the server subtree for a fresh client render.

★
Imagine two bakers following the same recipe in different kitchens.

Four offender families cause nearly every case. Time expressions like Date.now evaluate at different moments on each side. Random values like Math.random differ by contract. Locale formatters like toLocaleString depend on ICU data that varies between Node and browsers. Browser-only reads like window.innerWidth take one branch on the server and another on the client.

What it is NOT shapes the response. It isn't a routing bug or a data-fetching bug — the data is usually right on both sides, just formatted or timed differently. It isn't fixed by disabling server rendering wholesale, which forfeits performance to dodge discipline. And it isn't always an error: a live clock genuinely varies, which is why the suppression flag exists for single nodes.

Think of it as two choirs singing from the same score in different cities. The score — your render code — must produce the same notes from the same page. Time, dice, and local accents aren't in the score; they're improvisation. Hydration demands unison on first paint, then hands the client the solo after mount.

Plain-English First

Imagine two bakers following the same recipe in different kitchens. The server bakes at dawn with its own clock and local ingredients; the browser rebakes at noon with its clock and ingredients. If the recipe says decorate with today's date or a pinch of whatever's local, the cakes differ and the customer notices. Hydration is the customer comparing both cakes crumb by crumb. The fix is a recipe that bakes identically everywhere, then lets the browser add garnish after delivery.

You ship a Next.js page, open it, and the console warns: text content does not match server-rendered HTML. The page still works — React recovers — but first paint flickers, state resets inside the subtree, and you lose the speed you chose server rendering for.

The cause is almost always a render that isn't deterministic. The server ran it at one time with one locale and no window object; the client runs it a moment later with another locale and a full browser. Date.now, Math.random, toLocaleString, and window.innerWidth each guarantee the two outputs differ, and React's comparer is exact — one character off is a mismatch.

This surfaces hardest in clocks, greeting banners, formatted prices, and responsive layouts. They look innocent in development, where server and client share your machine's locale and clock, then break in production where they share nothing.

This guide covers the whole category. You'll learn what hydration compares, which expressions can never match, how to defer client values with useEffect, and the narrow cases where suppressHydrationWarning is the honest answer.

What Hydration Compares and Why Text Must Match Exactly

Hydration is React's reconciliation of two renders: the HTML string the server produced and the virtual tree the client builds on load. React walks both node by node, and for text nodes it compares character for character. Any difference — a timestamp, a locale comma, an extra space — fails the comparison for that node.

When a mismatch hits, React doesn't patch the text. It discards the server-rendered subtree and client-renders fresh. The page still ends up correct, which fools teams into treating the warning as cosmetic. But the recovery costs the exact benefits server rendering promised: fast first paint flashes into a re-render, and any state inside the replaced subtree resets to initial values.

The error message is genuinely helpful. It prints the server HTML snippet and the client text it expected instead, so the first differing character points at the guilty expression. Learning to read that quote — rather than reaching for the suppression flag — resolves most reports in minutes.

The rule that follows is absolute: the client's first render must be byte-identical to the server's output. Everything dynamic — clocks, randoms, browser measurements — belongs after mount, never in the render path. The next sections show each offender and its deferral pattern.

📊 Production Insight
Teams that treat the warning as cosmetic pay in flicker and reset state — the recovery path costs more than the fix every time.
🎯 Key Takeaway
Server HTML and the client's first render must agree character for character. Read the error's quote to find the guilty node fast.

Date.now, Math.random, and Locale Strings That Break Render

Date.now and new Date capture different moments on server and client by construction. The server renders at request time; the client hydrates milliseconds or seconds later. Even a one-second boundary crossing flips the text, and sub-second timestamps differ on literally every load. There is no clock value that matches across two renders.

Math.random is worse: its contract guarantees difference. Using it for ids, keys, or displayed values means the server and client can never agree. Keys deserve special mention because mismatched keys don't just warn — they scramble component identity, unmounting and remounting list items and destroying their state.

Locale formatting fails more subtly. toLocaleString and Intl.NumberFormat draw on ICU data that differs between Node versions and browsers. A price like 1,234.50 on your laptop renders as 1.234,50 in a German browser or with different grouping on a minimal-ICU server. Development hides this because server and client share your machine; production splits them across the world.

The shared trait is render-scope nondeterminism: any expression whose value depends on when, where, or in which runtime it runs. Audit renders for time, randomness, and locale — if the value can differ between two machines, it will differ between server and client.

clock-bad.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
// BAD: nondeterministic render — mismatches every load.
function ClockBad() {
  return <p>{new Date().toLocaleTimeString()}</p>;
}

// BAD: random id changes between server and client.
function IdBad() {
  const id = 'field-' + Math.floor(Math.random() * 10000);
  return <label htmlFor={id}>Name</label>;
}
Try it live
📊 Production Insight
Locale bugs survive every staging check when staging shares the dev region — production's global audience is the first real test.
🎯 Key Takeaway
Time, randomness, and locale can never match across renders. Keep all three out of the render path or mismatch on every load.

Reading window or navigator During Render Hydrates Wrong

The server has no window, no navigator, no matchMedia, and no localStorage. Code that reads them during render either crashes the server build outright or — when guarded with typeof checks — renders the fallback on the server and the real value on the client. Both branches produce text, and the texts differ.

Responsive layouts are the classic victim. Rendering mobile markup when window is undefined and desktop markup when it's 1440px wide guarantees a mismatch on every desktop load. The same applies to theme detection via matchMedia, language via navigator.language, and auth state via localStorage tokens.

The typeof window guard feels like a fix but only converts a crash into a mismatch. The server takes the fallback branch, the client takes the real branch, and hydration compares fallback text against real text. You've traded an exception for a flicker — better uptime, same broken first paint.

The correct shape renders the fallback on both passes, then upgrades after mount. Default state holds the server-safe value, useEffect reads the browser API and calls setState, and the client re-renders with the real value moments later. First paint matches, enhancement follows, and no warning fires.

width-guard.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BAD: window does not exist on the server.
function WidthBad() {
  const wide = window.innerWidth > 768;
  return <p>{wide ? 'Desktop view' : 'Mobile view'}</p>;
}

// GOOD: deterministic first render, adapt after mount.
import { useEffect, useState } from 'react';

function WidthGood() {
  const [wide, setWide] = useState(false);
  useEffect(() => {
    setWide(window.innerWidth > 768);
  }, []);
  return <p>{wide ? 'Desktop view' : 'Mobile view'}</p>;
}
Try it live
📊 Production Insight
Responsive-layout mismatches are the top cause of cart-state resets on commerce sites — the remounted subtree drops the flyout.
🎯 Key Takeaway
typeof window guards turn crashes into mismatches. Render the fallback on both passes and upgrade it in useEffect after mount.

Rendering Client-Only Values Safely With useEffect

The useEffect deferral pattern has three steps. First, pick a deterministic placeholder: a skeleton, a dash string, or the most likely server-safe value. Both server and client render it, so hydration matches trivially. Second, compute the real client value inside useEffect, which runs only on the client after mount. Third, store it in state so the component re-renders with live data.

Placeholders deserve care because users see them. A clock showing --:-- for one frame is fine; a layout that jumps when the real value arrives is not. Reserve space with fixed widths or skeleton blocks so the post-mount update swaps content without moving pixels. The goal is identical text at hydration and stable layout after enhancement.

This pattern also handles formatting correctly. Pass explicit locale and timeZone options inside the effect — the computation runs only in the browser, so only one ICU implementation matters. Server output stays a static placeholder with no locale dependency at all.

For data that arrives asynchronously, extend the same shape: placeholder, then fetch in the effect, then setState. The placeholder can even be the server-fetched value with client-only decorations applied after mount. Deterministic base plus client garnish covers nearly every real design.

local-time.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useEffect, useState } from 'react';

// Deterministic first paint, client value right after mount.
export function LocalTime({ epoch }) {
  const [label, setLabel] = useState('--:--');

  useEffect(() => {
    setLabel(
      new Date(epoch).toLocaleTimeString('en-US', { timeZone: 'UTC' })
    );
  }, [epoch]);

  return <time>{label}</time>;
}
Try it live
📊 Production Insight
Skeleton placeholders that reserve exact space cut layout-shift complaints to zero while keeping hydration deterministic.
🎯 Key Takeaway
Placeholder on both passes, real value in useEffect, setState to enhance. Stable layout plus matching text ends the flicker.

When suppressHydrationWarning Is the Correct Fix

suppressHydrationWarning exists for text that legitimately differs between server and client — a live timestamp, a timezone abbreviation, a value that updates every second by design. Adding it to that single element tells React to skip the text comparison there while hydrating everything else normally.

The keyword is single. The flag suppresses warnings only for the text directly inside its element, so scoping it to one span keeps every surrounding node fully checked. That's the honest use: you've audited the tree, everything deterministic matches, and one live value can't match by definition.

Misuse is covering a parent div to silence several mismatches at once. The warning stops but the underlying divergence remains — React still client-renders the differing parts, layout still shifts, and future real bugs hide behind the same flag. Reviewers should treat a broad flag like a disabled test: suspicious until proven necessary.

Prefer determinism first even for live values. A clock can render a placeholder and fill in after mount with no flag at all. Reserve suppression for cases where the placeholder itself harms the design — SEO-critical timestamps, for example. When you do use it, leave a comment naming why that node must vary.

suppress-legit.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Legitimate use: a live clock that must differ.
export function LiveClock() {
  return (
    <span suppressHydrationWarning>
      {new Date().toLocaleTimeString()}
    </span>
  );
}

// Pinned formatting: deterministic everywhere.
export function Price({ cents }) {
  const label = (cents / 100).toLocaleString('en-US', {
    style: 'currency',
    currency: 'USD',
    timeZone: 'UTC'
  });
  return <span>{label}</span>;
}
Try it live
⚠ Suppress Only the Node That Must Differ
If the flag covers more than one element, it's hiding a bug. Shrink it to the live node and fix the rest deterministically.
📊 Production Insight
Audits that shrink every flag to a single node routinely uncover second mismatches the broad flag had been masking for months.
🎯 Key Takeaway
One element, one live value, everything else deterministic. Broad flags hide bugs; narrow ones document intent.

Finding the Mismatched Node From the Hydration Error

Start from the error quote, not from guesses. Next.js prints the server text and the client text; the first divergence names the expression. Search the tree for that string's source — a date pipe, a price formatter, a conditional on window — and you've usually found the line within minutes.

Next, reproduce deterministically. Reload twice: values that change per reload implicate time or random; values stable per machine but different across machines implicate locale or browser APIs. Toggle timezones between runs to confirm locale suspects. Each pattern maps to its section above with a known fix.

Then verify structurally. View page source for the raw server HTML and compare it against the client's first render captured in a mount effect. After the fix, both must agree character for character while the post-mount enhancement still delivers live values. That two-step check — match at hydration, live after mount — is the acceptance test.

Finally, lock it with tests. Render the component to a string in Node and in a browser-like harness with differing locales and clocks, and assert identical output. Add a lint rule against Date.now, Math.random, and bare window reads in render scope. Hydration bugs are famously recurring; automation keeps them fixed.

📊 Production Insight
Dual-locale render tests catch in CI what staging never will — one run under UTC and one under Tokyo exposes every unpinned formatter.
🎯 Key Takeaway
Quote, repro, verify, lock. The error names the node, the pattern names the fix, and tests keep it fixed.
● Production incidentPOST-MORTEMseverity: high

Sale-Week Price Flicker That Traced to One Unpinned Locale

Symptom
During a sale week, every product page flashed unstyled prices for a beat on load, and the cart flyout closed itself whenever hydration recovered. Error tracking logged thousands of text-content mismatch warnings a day.
Assumption
The team assumed toLocaleString output was stable because staging matched: the staging server and every dev laptop shared the same locale and timezone. Nobody pinned formatting options.
Root cause
The price component called toLocaleString with no locale argument, and the header rendered new Date during render. Production servers ran with UTC locale while browsers used the shopper's locale, so every price and the greeting timestamp mismatched. React discarded the server HTML and client-rendered, causing a visible flicker and resetting the cart flyout state on each load.
Fix
Prices now format with explicit locale and currency options, and the greeting clock moved into a client-only component that renders a skeleton until useEffect supplies the time. suppressHydrationWarning covers only the clock span. Visual diff tests run under two timezones to lock the behavior.
Key lesson
  • Never rely on runtime defaults that differ between Node and browsers. Pin locale, timezone, and currency everywhere user-visible text is formatted.
  • Dev-prod parity must include locale and timezone, not just code and data. A staging server in the same region as your laptop proves nothing.
  • Recovering gracefully still costs you: React's client re-render forfeits the speed hydration was bought for. Deterministic first paint is the real fix.
Production debug guideFive checks that name the guilty node and confirm the deterministic fix.5 entries
Symptom · 01
Console shows the mismatch but not which component caused it
→
Fix
Read the full error text: it quotes the server HTML and the client text side by side. Copy both into a diff and find the first differing character. The component rendering that node is your suspect — open it and look for time, random, locale, or browser reads.
Symptom · 02
You suspect nondeterminism but the tree is large
→
Fix
Search the suspect component for Date.now, new Date, Math.random, toLocaleString, Intl formatters, window, navigator, and matchMedia at render scope. Any hit feeding JSX text is a candidate. Move each into useEffect; when the warning stops, you've found it.
Symptom · 03
Mismatch appears in production but never locally
→
Fix
Render the page with TZ set to UTC versus your local timezone. If the mismatch appears only under one setting, an unpinned locale or timezone is the cause. Pin timeZone and locale explicitly and re-test both.
Symptom · 04
Warning vanished but layout still shifts on load
→
Fix
Remove suppressHydrationWarning flags one by one and reload. If a mismatch surfaces under a removed flag, that flag was masking a real bug rather than covering a live value. Shrink each remaining flag to the single element that truly varies.
Symptom · 05
You need proof the fix worked before deploying
→
Fix
View page source, not DevTools Elements, to see the raw server HTML, then compare against the client's first render logged from a useEffect ref. The differing node confirms the diagnosis end to end, and after the fix both agree character for character.
Hydration Mismatch Causes Compared
Root CauseHow to ConfirmFixPrevention
Date.now or random in renderMismatch on every reload at the same node; values differ per refreshMove computation into useEffect stateLint against Date.now and Math.random in render
Locale-dependent formattingMatches locally, fails in production with a different server localePin locale and timeZone or format client-sideSnapshot tests with a fixed locale and TZ
window or navigator in renderServer HTML differs from client expectation; subtree remountsRead browser APIs in useEffect onlyKeep render pure; gate client reads behind mounted state
Legitimate live valueValue must differ by design, like a clocksuppressHydrationWarning on that one elementIsolate live nodes so the flag covers one line
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
clock-bad.jsfunction ClockBad() {Date.now, Math.random, and Locale Strings That Break Render
width-guard.jsfunction WidthBad() {Reading window or navigator During Render Hydrates Wrong
local-time.jsexport function LocalTime({ epoch }) {Rendering Client-Only Values Safely With useEffect
suppress-legit.jsexport function LiveClock() {When suppressHydrationWarning Is the Correct Fix

Key takeaways

1
Hydration compares server HTML against the client's first render exactly
one character off fails.
2
Date.now, Math.random, locale formatting, and window reads can never match across renders.
3
Compute client values in useEffect and render a deterministic placeholder first.
4
suppressHydrationWarning belongs on single legitimately-live elements, never whole subtrees.
5
Pin locale and timeZone explicitly; server and browser ICU data always differ somewhere.
6
Read the expected-versus-actual quote in the error
it names the guilty node.

Common mistakes to avoid

5 patterns
×

Calling Date.now or new Date() during render

Symptom
Every reload logs a hydration mismatch on the timestamp node, and the time flickers between server and client values.
Fix
Compute the value in useEffect and store it in state, rendering a placeholder or nothing until it arrives. Server and client agree on the first paint; the client enhances after.
×

Using Math.random for keys or ids in rendered output

Symptom
Hydration fails on every load with mismatched ids, and list state scrambles because keys differ between renders.
Fix
Generate the id in useEffect or with useId, which stays stable across server and client. Random values must never participate in first render.
×

Reading window or navigator during render

Symptom
Server renders one layout and the client expects another, so hydration replaces the whole subtree and interaction state resets.
Fix
Read window, navigator, or matchMedia in useEffect and keep the render output deterministic. Render a neutral default first, then adapt after mount.
×

Formatting with toLocaleString and no explicit locale

Symptom
Works on your machine but mismatches in production, where the server's ICU data differs from the user's browser.
Fix
Format dates and numbers with explicit locale and timeZone options, or defer formatting to the client. Never rely on runtime defaults that differ between Node and the browser.
×

Slapping suppressHydrationWarning on a whole subtree

Symptom
The warning vanishes but layout still shifts, and genuine mismatches elsewhere hide behind the same flag.
Fix
Scope it to the single node that legitimately varies, like a live timestamp, and keep surrounding markup deterministic. Audit each use so it never masks real bugs.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does text content does not match mean during hydration?
Q02SENIOR
Why do Date.now, Math.random, and locale formatting break hydration?
Q03SENIOR
How do you render a client-only value without mismatching?
Q04SENIOR
When is suppressHydrationWarning the correct fix?
Q05SENIOR
How do React Server Components change this picture?
Q01 of 05JUNIOR

What does text content does not match mean during hydration?

ANSWER
The server renders HTML, the client renders the same tree in memory, and React compares them node by node. Text must match exactly — one differing character discards the server node. The error quotes both versions so you can see which render produced the wrong value.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Does hydration fix mismatched text automatically?
02
Should I just wrap everything in dynamic imports with ssr false?
03
What exactly does suppressHydrationWarning suppress?
04
Does React StrictMode change hydration behavior?
05
Why do dates mismatch only in production?
06
How do I test components with client-only values?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's React.js. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
localStorage SecurityError Fix
52 / 52 · React.js