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
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
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.
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.
Related
- Debugging Hydration Failures Caused by Browser Extensions — separating third-party DOM edits from your own mismatches.
- Hydration Error Diagnostics — the full classification and bisection procedure.
- How to Debug Hydration Mismatch in Next.js App Router — the same problem with framework-specific tooling.
← Back to Hydration Error Diagnostics