This page narrows the broader Hydration Mismatch & State Recovery guide to one recurring source of noise: dates, times, and locale-formatted numbers that legitimately differ between server and client.
The exact failure this page solves #
A dashboard renders new Date().toLocaleString() for a “last updated” timestamp. The server runs in UTC inside a container; the visitor’s browser runs in Asia/Kolkata with a 12-hour clock preference. Server-rendered HTML says 7/6/2026, 2:00:00 PM; the client’s first paint says 7/6/2026, 7:30:00 PM. React’s hydration diff flags this as Text content does not match server-rendered HTML and, worse, throws it into the same bucket as an actual state bug — a broken if branch or a stale prop. The instinctive fix is to slap suppressHydrationWarning on the outer card component. That is exactly the wrong move: it silences the timestamp discrepancy, but it also silences the next unrelated mismatch that lands inside that card, which might be a real defect. The correct fix treats the timestamp as the only thing that is allowed to differ, and proves that by scoping suppression — or avoiding it entirely — to one leaf node.
Zero-to-working implementation #
The example below renders three variants side by side: the anti-pattern (commented out, do not ship it), a stable-then-reformat approach that needs no suppression at all, and a leaf-scoped suppression for a value that is inherently non-deterministic on every render.
// Timestamp.tsx
import { useEffect, useState } from 'react';
// ANTI-PATTERN — do not do this:
// <div suppressHydrationWarning>
// <span>{new Date().toLocaleString()}</span>
// {someOtherDynamicChild} {/* real bugs here go silent too */}
// </div>
interface TimestampProps {
isoDate: string; // e.g. "2026-07-06T09:15:00.000Z" — computed once, passed as a prop
}
// GOOD: server and first client render both show the neutral ISO value,
// so there is no mismatch and suppressHydrationWarning is unnecessary.
export function Timestamp({ isoDate }: TimestampProps) {
const [display, setDisplay] = useState(() => isoDate); // identical on server + client
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
// Runs only after hydration commits — reformatting here can never
// cause a mismatch because React has already reconciled the DOM.
const formatted = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(isoDate));
setDisplay(formatted);
}, [isoDate]);
return (
<time dateTime={isoDate} title={mounted ? undefined : 'Formatting…'}>
{display}
</time>
);
}
// GOOD: a value that is expected to differ (relative "time ago" text ticks
// every render), so suppression is scoped to the single text-bearing leaf.
export function RelativeTime({ isoDate }: TimestampProps) {
const [label, setLabel] = useState('just now'); // stable placeholder, both sides
useEffect(() => {
const update = () => setLabel(formatRelative(new Date(isoDate)));
update();
const id = setInterval(update, 15_000);
return () => clearInterval(id);
}, [isoDate]);
return (
<time dateTime={isoDate}>
{/* Only this span's text is allowed to differ; nothing else nearby is. */}
<span suppressHydrationWarning>{label}</span>
</time>
);
}
function formatRelative(date: Date): string {
const seconds = Math.round((date.getTime() - Date.now()) / 1000);
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
if (Math.abs(seconds) < 60) return rtf.format(Math.round(seconds), 'second');
return rtf.format(Math.round(seconds / 60), 'minute');
}
Step-by-step explanation #
- Pass a stable
isoDateprop, never a freshnew Date(). The value must be computed once — server-side during render, then re-sent unchanged as serialized props — so both the server markup and the client’s first render start from the identical string. - Seed
displaywithisoDateitself. BecauseuseState(() => isoDate)initializes to the same neutral value on both sides, React’s hydration diff finds nothing to complain about on first paint. - Reformat inside
useEffect, after mount. The effect only runs once hydration has committed, so callingsetDisplaythere triggers an ordinary post-hydration re-render, not a hydration mismatch — this is the safest and most common fix, and it needs zerosuppressHydrationWarningcalls. - Reserve
suppressHydrationWarningfor values that cannot be made stable.RelativeTimeticks every 15 seconds by design; the label is supposed to differ between server render time and client hydration time. Attaching the prop to the<span>leaf — not the<time>wrapper, not a parent<div>— tells React “this exact node’s text is expected to diverge” without silencing anything else. - Keep
dateTimemachine-readable regardless of formatting path. Both components setdateTime={isoDate}on the<time>element so screen readers, crawlers, andDate.parseconsumers always get the unambiguous UTC value, independent of whichever human-readable string is currently displayed. - Confirm suppression didn’t leak. Add an unrelated, intentionally broken child in a test build and verify React still reports its mismatch in the console — proof the suppression is scoped correctly and not silencing the whole subtree.
Edge cases #
| Scenario | Symptom | Fix |
|---|---|---|
suppressHydrationWarning placed on a parent <div> |
Silences that node’s direct text/attribute mismatch only — but any nested element with its own mismatch still warns, giving a false sense that the parent is “safe” for everything inside it | Move the prop onto the exact leaf node whose content is expected to differ; audit every other child independently |
Intl.DateTimeFormat with no explicit locale, server vs client |
Server formats using the container’s default locale (often en-US or C), client uses the visitor’s OS/browser locale, producing different digit grouping or date order |
Either pass an explicit shared locale from a cookie/header on the server, or defer all locale-aware formatting to a post-mount useEffect as shown above |
Relative-time components (dayjs().fromNow(), react-timeago) |
Value depends on wall-clock time at render, so server and client almost always disagree by the network round-trip duration | Render a static placeholder ("just now") for the initial paint, compute the real relative string in useEffect, and suppress only that leaf if it must render before the effect fires |
<time> element missing dateTime attribute |
Passes visually but fails accessibility and structured-data checks; screen readers announce the locale string with no unambiguous fallback | Always set dateTime to the canonical ISO string, independent of which human-readable text is currently shown |
Blanket suppressHydrationWarning on the whole page/root component |
A real regression (stale cache, broken conditional, undefined prop) renders wrong content on both server and client and never surfaces as a warning | Never apply the prop above the individual leaf; treat any temptation to suppress a whole subtree as a signal the data flow needs a stable-value rewrite instead |
Verification steps #
- Disable JavaScript throttling and hard-reload with SSR on. Open DevTools Console before the client bundle executes; confirm there is no
Text content does not match server-rendered HTMLwarning for the timestamp component. - Force a locale mismatch deliberately. Set the server’s
TZenvironment variable toUTCand your browser’s locale to a different timezone; reload and confirm the console stays clean while the displayed time still updates correctly after mount (visible flash from ISO placeholder to localized string is expected and fine). - Break something on purpose. Temporarily render
condition ? <A /> : <B />whereconditiondiffers between server and client (e.g., readingtypeof window) inside the same subtree asRelativeTime. Confirm React still logs a hydration warning forA/B— proof the leaf-scoped suppression onRelativeTimedid not swallow it. - Automated assertion. In a Playwright or Testing Library test, capture
console.errorcalls during a simulated SSR-then-hydrate flow and assert the call count is zero for any string containing"did not match". - Accessibility check. Inspect the rendered
<time>element in DevTools and confirmdateTimealways holds a valid ISO 8601 string, regardless of which formatting branch produced the visible text.
Frequently asked questions #
Does suppressHydrationWarning hide mismatches in child elements too?
No. It only silences a text or attribute mismatch on the exact DOM node it is attached to. React still walks the rest of the tree normally and reports any other mismatch it finds, including mismatches on that node’s own children, so scoping it to a single leaf is both sufficient and safe.
Why not just wrap the whole date component in suppressHydrationWarning to be safe?
Because “safe” here actually means “silent.” Wrapping a container hides every future mismatch inside it, including a broken conditional, stale server data, or a locale bug you have not found yet. Keeping the prop on one known-divergent leaf preserves the warning as a signal for everything else nearby.
Is Intl.DateTimeFormat available during server-side rendering?
Yes, modern Node.js ships with full ICU data, so Intl.DateTimeFormat and Intl.NumberFormat work identically on the server and client. The mismatch is not a missing-API problem; it happens because the server does not know the visitor’s timezone or locale unless you pass one explicitly, so it falls back to its own environment defaults.
Related #
- Hydration Mismatch & State Recovery — parent guide covering the full range of server/client divergence causes and recovery patterns
- Fixing “Text Content Does Not Match” Hydration Errors in React — the general-purpose diagnostic workflow this page’s date/locale case narrows down from
- Session State Persistence & Hydration Fallbacks — broader coverage of client-side durability and rehydration strategies this guide sits under