Hydration-Safe Rendering for Money and Time
new Date() and Math.random() in a React render body cause hydration mismatches — and on a billing dashboard, a flicker on a number erodes trust. Here is the pattern that keeps server and client agreeing.
On a dashboard that shows people their money, a flicker is not cosmetic — it's a trust problem. If a balance or a timestamp renders one value on the server and a different value after hydration, React throws error #418 and the number visibly jumps. The cause is almost always the same: reading a non-deterministic value during render. This post is why that happens and the one pattern that fixes it.
Why server and client disagree
A server-rendered React app renders twice: once on the server (to send HTML) and once on the client (to hydrate it). Hydration assumes the two renders produce identical output. Anything non-deterministic read during render breaks that assumption:
// ❌ each of these differs between server and client render
function Greeting() {
const hour = new Date().getHours(); // server TZ vs client TZ
const id = Math.random(); // different every call
const seen = localStorage.getItem('x'); // undefined on server
return <span>{hour < 12 ? 'Morning' : 'Afternoon'}</span>;
}The server computes getHours() in its timezone; the client computes it in the user's. Different value, mismatched HTML, hydration error #418, visible flicker. Math.random() and localStorage (which doesn't exist server-side) fail the same way.
useState(() => new Date()) does NOT fix it
The tempting fix — a lazy state initializer — doesn't work, because the lazy initializer also runs on the server during SSR. The server still computes a value, still sends it in the HTML, and the client still diverges. The initializer being "lazy" changes when it runs, not where. You have to render a neutral value first.
The pattern: neutral default, then useEffect
The fix is to render a deterministic placeholder on the server and overwrite it with the real value after mount, where only the client runs:
function Greeting() {
const [greeting, setGreeting] = useState('Hello'); // neutral, same on both
useEffect(() => {
const h = new Date().getHours(); // client-only, post-hydration
setGreeting(h < 12 ? 'Morning' : 'Afternoon');
}, []);
return <span>{greeting}</span>;
}Server and client both render Hello initially — identical HTML, clean hydration — and the real greeting appears a tick later on the client. useEffect never runs on the server, so anything inside it (time, randomness, localStorage, window) is safe.
Why this matters extra for a billing dashboard
On a marketing page a hydration flicker is ugly. On a dashboard showing money, it's corrosive: a balance or spend figure that renders $0.00 then jumps to $412.19, or a "last updated" time that flickers, makes a user wonder whether the number itself is trustworthy. The numbers are the product. So the rule is enforced as a review gate: no new Date(), Date.now(), Math.random(), crypto.randomUUID(), localStorage, or window.* in a client component's render body. Money and time values follow the neutral-default-then-useEffect pattern, every time.
Static pages: bake at build, not at render
For statically pre-rendered pages the same constraint applies, just shifted earlier: compute time-dependent values at build (via the framework's static-params/metadata hooks), never at request render. A "published 3 days ago" that's computed at render will be wrong the moment the page is cached and served later. Bake the absolute date at build and format relative-time on the client in useEffect if you need "3 days ago" to stay live.
Catching it before it ships
Because the failure is easy to reintroduce (someone adds a new Date() to a render body without thinking), it's a grep-able review gate, not just a convention. A reviewer (human or automated) scans changed client components for the banned reads and flags them before merge. A rule that depends on everyone remembering it will regress; a rule that's checked on every diff won't. (Same philosophy as the parity checks — verify the invariant, don't assume it.)
The takeaway
Hydration mismatch comes from reading non-deterministic values — time, randomness, localStorage — during render, and a lazy state initializer doesn't save you because it runs on the server too. Render a neutral default and overwrite it in useEffect after mount; bake static values at build. On a dashboard where the numbers are the product, a stable render isn't polish — it's the difference between a balance that looks authoritative and one that looks like it's guessing.