Next.js error
Text Content Does Not Match Server-Rendered HTML
The error
Warning: Text content did not match. Server: "2 hours ago" Client: "3 hours ago"The narrower cousin of a hydration mismatch, and it is nearly always one of three things: a relative time, a locale-formatted number, or whitespace.
This is a hydration mismatch confined to a text node, which makes it easier to diagnose than the general case: React prints both values, so you can usually see the cause in the message itself.
Three causes account for almost all of them.
1. Relative time
"2 hours ago", "just now", "in 3 days". The server computes it at render
time; the browser computes it again at hydration time. If a boundary fell
between the two - a minute, an hour, midnight - the strings differ.
Render the absolute value on both passes and upgrade after mount:
'use client';
export function RelativeTime({ iso }: { iso: string }) {
const [relative, setRelative] = useState<string | null>(null);
useEffect(() => setRelative(formatRelative(iso)), [iso]);
return (
<time dateTime={iso} title={new Date(iso).toISOString()}>
{relative ?? formatAbsolute(iso)}
</time>
);
}The HTML contains a real date, which is also what a crawler and a screen reader should receive. The relative form is a convenience that arrives a moment later.
2. Locale-dependent formatting
toLocaleString(), toLocaleDateString(), Intl.NumberFormat with no
explicit locale. The server takes its locale and timezone from the Node
process - typically en-US and UTC. The browser takes the user's. 1.234,56
on one side and 1,234.56 on the other.
Always pass the locale and the timezone explicitly:
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n);
new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin' }).format(d);If the locale genuinely depends on the visitor, it has to reach the server - from the URL segment or a cookie - so both renders agree. That is what a locale-aware routing setup is for, and it is the reason the locale belongs in the route rather than in client state.
3. Whitespace around an expression
The subtlest one, and it looks like a formatter's fault:
<span>
{count} items
</span>JSX collapses the newline and indentation into a single space here, which is
usually what you want and occasionally not - particularly around punctuation
or inside white-space: pre. If the message shows two strings differing only
by a space, this is it. Write the expression on one line, or make the space
explicit with {' '}.
What looks like a fix and is not
suppressHydrationWarning on the parent. It applies one level deep, so on
a wrapper it silences nothing useful, and where it does apply it hides the
warning without preventing the re-render. On a single <time> element whose
difference you have decided is correct, it is legitimate. As a way to make a
list of mismatches quiet, it is not.
Rendering the whole component client-side. The mismatch goes away because the server render does. For a timestamp inside otherwise static content, that is a large trade for a small label.
Whether it is worth chasing
One mismatched label costs almost nothing. The same component repeated across a list means React discards and re-renders the list, and a mismatch high in the tree can take most of the page with it - which shows up as a Largest Contentful Paint that no amount of image work improves.
The console counts them. If there are more than a handful on a page, that is a measurement, not a warning.
