# Hydration-Safe Date Rendering

**MUST** · **ID:** `performance-hydration-safe-dates` · **Category:** performance
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/performance-hydration-safe-dates

> MUST: Render dates deterministically on the server — a fixed locale and time zone, or an ISO string inside `<time dateTime>` — and localize inside `useEffect` after hydration. Calling `toLocaleString()`/`new Date()` during render mismatches between a UTC server and the user's zone, discarding the SSR subtree.

Guard date and time rendering against server/client hydration mismatches

Calling toLocaleString() (or new Date()) during render produces one string on a server running in UTC and a different one in a browser set to Tokyo or Berlin. React compares the two, finds the text node does not match, discards the server-rendered subtree and re-renders it client-side — the exact work SSR was supposed to save. Render a deterministic value first (a fixed time zone and locale, or an ISO string inside <time dateTime>), then localize inside useEffect once hydration has finished.

## Bad — do not do this

`performance-hydration-safe-dates-bad`

```tsx
import { useState } from 'react';

const TIMESTAMP = Date.UTC(2026, 0, 15, 23, 30);

/** What a server running in UTC / en-US would have put in the HTML. */
const serverHtml = new Intl.DateTimeFormat('en-US', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: 'UTC',
}).format(TIMESTAMP);

export function HydrationSafeDatesBad() {
  const [nonce, setNonce] = useState(0);

  // Formats with the *browser's* locale and time zone — a different string than the server sent.
  const clientRender = new Date(TIMESTAMP).toLocaleString();
  const mismatch = clientRender !== serverHtml;

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="space-y-3 rounded-lg border border-border bg-card p-4" key={nonce}>
        <div className="space-y-1">
          <div className="text-xs text-muted-foreground">Server HTML (UTC, en-US)</div>
          <div className="rounded bg-muted px-2 py-1 font-mono text-xs text-foreground">
            {serverHtml}
          </div>
        </div>
        <div className="space-y-1">
          <div className="text-xs text-muted-foreground">Client render (your locale + zone)</div>
          <div className="rounded bg-muted px-2 py-1 font-mono text-xs text-foreground">
            {clientRender}
          </div>
        </div>

        {mismatch ? (
          <div className="rounded-md border border-error/40 bg-error/10 px-2 py-1.5 text-xs text-error">
            Hydration mismatch: text content did not match. React throws away the server HTML and
            re-renders the whole subtree on the client.
          </div>
        ) : (
          <div className="text-xs text-muted-foreground">
            Your browser happens to be in UTC/en-US, so the strings agree here — they would not for
            most of your users.
          </div>
        )}

        <button
          type="button"
          onClick={() => setNonce((n) => n + 1)}
          className="rounded-lg bg-primary px-3 py-1.5 text-xs text-primary-foreground hover:bg-primary/90 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
        >
          Replay hydration
        </button>
      </div>
      <p className="text-xs text-error">
        <code>toLocaleString()</code> during render produces one string on the server and a different
        one in the browser. Every visitor outside the server&apos;s time zone pays for a discarded
        server render.
      </p>
    </div>
  );
}
```

## Good — do this

`performance-hydration-safe-dates-good`

```tsx
import { useEffect, useState } from 'react';

const TIMESTAMP = Date.UTC(2026, 0, 15, 23, 30);
const ISO = new Date(TIMESTAMP).toISOString();

/** Deterministic: same string on the server and on the client's first paint. */
const serverHtml = new Intl.DateTimeFormat('en-US', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: 'UTC',
}).format(TIMESTAMP);

function SafeDate() {
  // First paint renders exactly what the server sent...
  const [label, setLabel] = useState(serverHtml);
  const [localized, setLocalized] = useState(false);

  // ...then, after hydration is done, upgrade to the visitor's locale and zone.
  useEffect(() => {
    setLabel(new Date(TIMESTAMP).toLocaleString());
    setLocalized(true);
  }, []);

  return (
    <>
      <div className="space-y-1">
        <div className="text-xs text-muted-foreground">Server HTML (UTC, en-US)</div>
        <div className="rounded bg-muted px-2 py-1 font-mono text-xs text-foreground">
          {serverHtml}
        </div>
      </div>
      <div className="space-y-1">
        <div className="text-xs text-muted-foreground">
          Client first paint {localized ? '→ upgraded after mount' : ''}
        </div>
        <time
          dateTime={ISO}
          className="block rounded bg-muted px-2 py-1 font-mono text-xs text-foreground"
        >
          {label}
        </time>
      </div>
      <div className="rounded-md border border-success/40 bg-success/10 px-2 py-1.5 text-xs text-success">
        Markup matched on hydration. The localized string is applied in an effect, so React never has
        to discard the server render.
      </div>
    </>
  );
}

export function HydrationSafeDatesGood() {
  const [nonce, setNonce] = useState(0);

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="space-y-3 rounded-lg border border-border bg-card p-4">
        <SafeDate key={nonce} />
        <button
          type="button"
          onClick={() => setNonce((n) => n + 1)}
          className="rounded-lg bg-primary px-3 py-1.5 text-xs text-primary-foreground hover:bg-primary/90 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
        >
          Replay hydration
        </button>
      </div>
      <p className="text-xs text-success">
        Render a deterministic string (fixed zone/locale, wrapped in <code>&lt;time
        dateTime&gt;</code>), then localize in <code>useEffect</code>. Hydration matches for every
        visitor, whatever their time zone.
      </p>
    </div>
  );
}
```

## References

- [hydrateRoot: handling mismatches](https://react.dev/reference/react-dom/client/hydrateRoot)
- [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)
