# Fix date and time hydration errors

> Why new Date() and Date.now() cause a Next.js date hydration error, and three fixes: pass the server time as a prop, render after mount, or format the date.

Source: https://hydration.jscrate.dev/docs/causes/time
Last updated: 2026-09-18

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:

```text
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](https://hydration.jscrate.dev/docs/issues/hp1001) (text) or
[HP1002](https://hydration.jscrate.dev/docs/issues/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:

```tsx title="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()} />;
}
```

```tsx title="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:

```tsx title="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](https://hydration.jscrate.dev/docs/guides/useeffect-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](https://hydration.jscrate.dev/docs/causes/timezone) and
[locale](https://hydration.jscrate.dev/docs/causes/locale) differences for that half of the problem.

## How to solve hydration errors related to dates in React without hiding them

`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](https://hydration.jscrate.dev/docs/guides/suppresshydrationwarning).

## Catch it with ESLint

[`no-date-in-render`](https://hydration.jscrate.dev/docs/rules/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:

```bash
npm install -D eslint-plugin-hydration-proof
```

[`no-timezone-without-explicit-timezone`](https://hydration.jscrate.dev/docs/rules/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](https://hydration.jscrate.dev/docs/probes).

```bash
npx hydration-proof test --probe
```

## Related

- [Timezone hydration mismatches](https://hydration.jscrate.dev/docs/causes/timezone)
- [toLocaleString and locale formatting](https://hydration.jscrate.dev/docs/causes/locale)
- [Text content does not match server-rendered HTML](https://hydration.jscrate.dev/docs/errors/text-content-does-not-match-server-rendered-html)
- [HP1001: text differs between server and client](https://hydration.jscrate.dev/docs/issues/hp1001)
- [All causes of hydration errors](https://hydration.jscrate.dev/docs/causes)
