Fixing “Text Content Did Not Match” Warnings

“Text content did not match” is the most common hydration warning and the easiest to fix once you stop reading the component name and start looking at the value. It is always the same shape: one node, two different strings, one differing input. This walkthrough resolves it end to end, as the value-class instance of the procedure in hydration error diagnostics.

Prerequisites

Diagnostic Steps

Five Inputs, Five Signatures Time produces a value that drifts by seconds or crosses a date boundary. Time zone produces a value that differs by hours and only for some visitors. Locale changes separators and month names. Randomness changes an identifier every render. An environment read changes a branch entirely. Each signature identifies the input without reading any code. SIGNATURE IN THE TWO VALUES INPUT "2 minutes ago" versus "3 minutes ago" current time "4 March" versus "3 March" time zone "1,240.50" versus "1.240,50" locale "id-8f3a" versus "id-b21c" randomness present on one side only — an environment read such as a window check

Step 1 — Get both values in front of you

The server value is in the HTML; the client value is in the warning, or one console log away.

# The server's version of the node, straight from the response.
curl -s https://example.test/reviews | grep -o '<time[^>]*>[^<]*</time>' | head -3
# → <time datetime="2026-03-04T09:15:00Z">2 minutes ago</time>
// The client's version: log during the first render, before any effect runs.
export function PostedAt({ iso }) {
  const label = formatRelative(iso);           // ← the suspect
  if (typeof window !== 'undefined') console.log('[client first render]', label);
  return <time dateTime={iso}>{label}</time>;
}
// → [client first render] 3 minutes ago

Two strings, one input: the clock advanced between the server render and the browser render. Nothing else in the component matters.

Step 2 — Choose the fix that matches the input

Fix A — format on the server and pass the string. Best when the value does not depend on the visitor.

// Server: compute once, ship text. The island receives a string it renders verbatim.
const posted = { iso: review.postedAt, label: formatRelative(review.postedAt) };
// Client component becomes trivial and deterministic:
export const PostedAt = ({ iso, label }) => <time dateTime={iso}>{label}</time>;

Fix B — render an absolute value, upgrade after mount. Best when the value genuinely depends on the visitor’s clock or zone.

export function PostedAt({ iso }) {
  // First client render MUST equal the server render: absolute, zone-explicit.
  const [label, setLabel] = useState(() => formatAbsolute(iso, 'UTC'));
  useEffect(() => {
    // Runs after hydration, so no comparison happens against this value.
    setLabel(formatRelative(iso));
  }, [iso]);
  return <time dateTime={iso}>{label}</time>;
}

Fix C — pass the locale and zone explicitly. Best when the mismatch is formatting rather than time.

// Never rely on the ambient locale: the server's differs from the visitor's.
const label = new Intl.NumberFormat(request.locale, {
  style: 'currency', currency: 'EUR',
}).format(price);

Verification

Choosing Between the Three Fixes Formatting on the server removes the library from the bundle and produces one value for everyone, which suits caching but cannot reflect a visitor's own time zone. Deferring to an effect is correct per visitor at the cost of one extra frame and a slightly larger island. Passing an explicit locale keeps formatting on the server while remaining correct per request, but requires the locale to be known at render time. FIX GAINS COSTS A · server formats smaller island, cache-friendly cannot use the visitor's zone B · effect upgrade correct per visitor one extra frame, bigger island C · explicit locale server-side and per request locale must be known at render

After applying a fix, three checks confirm it landed. The console must be clean on a production build with an empty cache. The node must show the correct value on first paint with JavaScript disabled — Fix B in particular is easy to get wrong in a way that leaves an absolute value visible forever if the effect never runs. And the determinism test below must pass in a process with a different time zone than your laptop, which is what catches the class of bug that only appears for visitors on the other side of a date boundary.

test('PostedAt is deterministic', () => {
  process.env.TZ = 'Pacific/Auckland';          // deliberately not the server's zone
  const html1 = renderToString(<PostedAt iso="2026-03-04T09:15:00Z" label="4 March" />);
  const html2 = renderToString(<PostedAt iso="2026-03-04T09:15:00Z" label="4 March" />);
  expect(html1).toBe(html2);
  expect(html1).not.toMatch(/ago/);              // relative values must not be here
});

Why This Class Dominates

Value mismatches outnumber every other class in real codebases for a structural reason: formatting is the one thing components do that depends on the environment rather than on the data. Structure depends on props, attributes depend on props, order depends on the collection — but a formatted date depends on a clock, a zone and a locale that all differ between the machine that rendered the HTML and the machine that hydrates it.

That is also why the fix generalises into a rule worth applying before any warning appears: the server decides what the text says, the client decides how it behaves. A component that receives strings and attaches handlers cannot produce a value mismatch, because it does not compute values. Applying that rule across a codebase removes the class rather than the instance, and it has the side effect of shrinking island bundles, since formatting libraries stop being imported into client code — the payload argument made in replacing heavy dependencies in island code.

The residual cases are genuine: a countdown, a “time until your session expires”, a value in the visitor’s own zone. Those are exactly the cases where Fix B is correct, and where a deliberate suppression annotation is honest rather than lazy. Keeping that list short is the goal; keeping it empty is not realistic.

The Rule That Removes the Class Server decides what the text says, client decides how it behaves. A component that receives formatted strings and attaches handlers cannot produce a value mismatch, because it computes no values. One rule, applied everywhere server: formats dates, numbers and currency into strings the visitor-independent work happens once, where the data is client: attaches handlers and updates values on interaction no formatting, therefore no divergence possible exception: values that depend on the visitor own clock or zone — defer to an effect

Troubleshooting

Which fix should I choose when the value is a formatted date?

Format on the server and pass the string. It removes the mismatch, removes the formatting library from the island bundle, and produces the same output for every visitor — which is also what a cache wants. The exception is a value that must reflect the visitor's own time zone, which the server cannot know on a first request. There, render an absolute value the server can produce, then replace it with the local rendering in an effect after mount.

Is suppressing the warning on one element acceptable?

Only when the divergence is deliberate and unavoidable — a relative timestamp you have decided to render client-side, for example. Suppression silences the console; it does not stop the framework doing the extra work of reconciling the difference, and it hides any future mismatch on that node. Treat it as an annotation that says "this difference is intended", not as a fix.

Why does the warning appear only for some visitors?

Because the differing input varies by visitor. A time zone offset produces a mismatch only for visitors whose local date differs from the server's at that moment. A locale produces one only for visitors whose formatting rules differ from the build machine's. That is why the rate matters more than the individual report, and why a determinism test with a fixed locale and time zone is the reliable way to catch these before release.

← Back to Hydration Error Diagnostics