Hydration Proof

Search documentation

Find a page or section

Fix date and time hydration errors

The server renders at one moment, the browser at another.

A Next.js date hydration error happens when a component reads the current time while it renders: the server renders at one moment, the browser hydrates a few hundred milliseconds (or, for a cached page, days) later, and the text no longer matches. Pass the time the server used as a prop, or read the clock after the page has hydrated.

Symptoms

The same bug shows up under several names, depending on the React version and the build:

Hydration failed because the server rendered text didn't match the client.
Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "12:04:31" Client: "12:04:33"
Minified React error #418; visit https://react.dev/errors/418

hydration-proof reports it as HP1001 (text) or HP1002 (an attribute such as datetime), with the cause Time-dependent value. In the package's own test suite that cause is identified with 97% confidence.

A date hydration error that only appears on some visits usually means the value is rounded ("3 minutes ago"): the two renders differ only when they fall on either side of a boundary.

Why it happens

Any of these in render code produces a new value on each render:

  • Date.now(), new Date() and Date() with no arguments
  • performance.now() and Temporal.Now.*()
  • relative formatting built on them: "3 minutes ago", countdowns, "today"

React compares the first client render with the server HTML. A new Date() hydration mismatch or a Date.now() hydration mismatch is guaranteed as soon as the clock moves between the two renders, and it always does.

How to fix it

Pass the server's time as a prop

Read the clock once, on the server, and send the value to the client. Both renders then use the same number:

app/post/[slug]/page.tsx
// A Server Component: it only renders on the server.
export default async function Page() {
  const post = await getPost();
  return <PostDate publishedAt={post.publishedAt} renderedAt={Date.now()} />;
}
app/post/[slug]/post-date.tsx
"use client";
 
export function PostDate({
  publishedAt,
  renderedAt,
}: {
  publishedAt: number;
  renderedAt: number;
}) {
  const minutes = Math.round((renderedAt - publishedAt) / 60_000);
  return <p>Updated {minutes} minutes ago</p>;
}

Render the time after hydration

When the value must be live (a clock, a countdown), render a placeholder on the server and fill it in from an effect. Effects never run on the server, so the first client render still matches:

countdown.tsx
"use client";
 
import { useEffect, useState } from "react";
 
export function Countdown({ endsAt }: { endsAt: number }) {
  const [now, setNow] = useState<number | null>(null);
 
  useEffect(() => {
    setNow(Date.now());
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);
 
  if (now === null) return <span>–</span>;
  return <span>{Math.round((endsAt - now) / 1000)}s left</span>;
}

useEffect and two-pass rendering explains the pattern and when the flash of the placeholder matters.

Format a fixed date, not "now"

A date built from a stored value is deterministic: new Date(post.publishedAt) is the same on both sides. It can still differ if you format it in the reader's timezone or locale; see timezone and locale differences for that half of the problem.

suppressHydrationWarning on the element that shows the time makes React keep the server text without reporting it. That is acceptable for a live clock whose first value does not matter, and wrong everywhere else: the page shows the server's stale value until the next render. See when suppressHydrationWarning is safe.

Catch it with ESLint

no-date-in-render reports Date.now(), new Date(), performance.now() and Temporal.Now in render code, and skips Server Components with the next preset:

npm install -D eslint-plugin-hydration-proof

no-timezone-without-explicit-timezone catches the formatting half.

Catch it in CI

hydration-proof test loads every route and reports each time-dependent value as HP1001 or HP1002. With --probe, it reloads the page with the browser clock moved by 3 days, 7 hours, 11 minutes and 13 seconds; if the client value changes, the cause is proven rather than guessed. See probes.

npx hydration-proof test --probe