Next.js Hydration Mismatch — Date and Random Are Usual Suspects
Console: Text content does not match server-rendered HTML / hydration failed. The page flickers. Someone adds suppressHydrationWarning on <html> and declares victory. That flag hides the symptom on that node; it does not fix the divergent render. In Next.js apps the usual culprits are time, randomness, and browser-only APIs used during the initial render.
Why hydration cares
SSR HTML must match the first client render of the same component tree. React attaches event handlers to that HTML. If text or structure differs, React complains and may discard the server tree — which costs you the performance win you shipped SSR for, and leaves a visible flash.
// diverges every request vs every client
export default function Stamp() {
return <span>{new Date().toLocaleString()}</span>;
}
Server rendered 10:00:01 in UTC machine time; client renders local 19:00:01. Mismatch.
<span>{Math.random()}</span>
<span>{crypto.randomUUID()}</span>
Same class of bug: the server picked one value, the browser picked another before React could reconcile.
Fix patterns for dates
1. Render a stable placeholder, fill on the client
"use client";
import { useEffect, useState } from "react";
export function ClientTime({ iso }: { iso: string }) {
const [text, setText] = useState(iso); // same on server + first client paint
useEffect(() => {
setText(new Date(iso).toLocaleString());
}, [iso]);
return <time dateTime={iso}>{text}</time>;
}
2. Use UTC (or fixed locale) on both sides for the initial paint when you must show a clock without a flash.
3. suppressHydrationWarning on a <time> leaf — acceptable for true locale-dependent clocks, not a blanket on layout shells.
Browser-only APIs
// bad in RSC or first client render
const width = window.innerWidth;
const theme = localStorage.getItem("theme");
Gate with useEffect, or CSS media / color-scheme for theme to avoid flash without reading localStorage during SSR. For window, render a default and update after mount. Feature flags read from cookies on the server must use the same cookie value on the first client pass — do not re-roll A/B assignment in the browser during render.
Invalid HTML nesting
Hydration errors also come from illegal DOM (<p> wrapping <div>, <a> wrapping <a>). React’s message sometimes looks like a text mismatch. Validate the structure before chasing dates. MDX and CMS HTML are frequent sources: a content author pastes a block that breaks nesting only in production builds with different sanitizers.
Third-party widgets
Embeds that inject different markup on server vs client need dynamic(() => import('./Widget'), { ssr: false }) or a client-only mount. Do not SSR a component that calls document at module scope. Chat widgets, cookie banners, and map SDKs love to mutate the DOM before React hydrates — isolate them below the fold when you can.
Debugging workflow
- Read the diff in the console — React often shows server vs client text.
- Bisect: comment out suspects (headers with dates, A/B flags, feature toggles from cookies).
- Search for
Date.now,toLocaleString,Math.random,uuid,localStorage,window. - Confirm you are not mutating
childrendifferently based ontypeof window !== 'undefined'during render (use effect instead). - Use
suppressHydrationWarningonly on the specific text node that intentionally differs. - In App Router, check whether a Server Component is leaking non-serializable or environment-specific values into a Client Component’s props.
App Router notes
Server Components should not use browser APIs. Pass serializable props (ISO strings) into Client Components that format after mount. Random IDs for list keys must be generated where they stay stable — prefer database IDs, not Math.random() in render. If you need a client-only id for accessibility, generate it once with useId() so server and client share the same algorithm.
Streaming and Suspense do not excuse mismatched first paint: each streamed chunk still has to hydrate consistently with what the server emitted for that boundary.
When suppressHydrationWarning is fine
A live clock that must show the user’s timezone is a legitimate leaf-level exception. Root layout suppression because “the warning is annoying” is not. If half the page mismatches, you have a determinism bug, not a console noise problem.
Hydration mismatches are determinism bugs. Make the first client render agree with the server; decorate with locale and randomness after mount. That is the fix — not silencing the console on the root layout.