# suppressHydrationWarning Only Where Truly Needed

**NEVER** · **ID:** `interactions-hydration-warning-suppression` · **Category:** interactions
**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/interactions-hydration-warning-suppression

> NEVER: Put `suppressHydrationWarning` on a subtree or layout wrapper to quiet mismatch noise — it keeps the wrong server value on screen. Apply it to the single element rendering a genuinely unstable value (`Date.now()`, a local clock, a random id) and fix every other mismatch at the source.

Reserve suppressHydrationWarning for values that genuinely cannot match, never as a way to quiet noise

A hydration warning is React telling you the server markup and the client render disagree — which for deterministic data is a real bug (a locale, a timezone, a feature flag read differently on each side). `suppressHydrationWarning` does not fix the mismatch: React keeps the server text and stops reporting it, so the user is left staring at the wrong value and nothing warns anyone. Only genuinely unstable values qualify: `Date.now()`, a formatted local clock, a random id. Apply it to that one element, never to a subtree or a layout wrapper, and fix everything else at the source — render a stable placeholder on the server and compute the client-dependent value after mount.

## Bad — do not do this

`interactions-hydration-warning-bad`

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

// What the server emitted into the HTML, in the server's own locale.
const SERVER_HTML = { price: '$1,234.50', renderedAt: '10:04:07' };
// What this client would render for the same data.
const CLIENT_RENDER = { price: '1 234,50 €', renderedAt: '13:22:41' };

export function HydrationWarningBad() {
  const [hydrated, setHydrated] = useState(false);

  // suppressHydrationWarning tells React: keep the server text, say nothing. It is on both
  // fields, so the currency bug is silenced along with the clock and the server text survives.
  const shown = SERVER_HTML;
  // The clock is allowed to differ. The price is not — it is deterministic data, so a
  // difference there is a bug, and suppression is the reason nobody hears about it.
  const deterministic = ['price'] as const;
  const silentlyWrong = hydrated
    ? deterministic.filter((k) => SERVER_HTML[k] !== CLIENT_RENDER[k])
    : [];

  return (
    <div className="w-full max-w-sm">
      <div className="bg-card border border-border rounded-lg p-4">
        <p className="text-xs text-muted-foreground mb-3">
          Both fields carry <code>suppressHydrationWarning</code>. Press Hydrate and watch what the user is left
          looking at.
        </p>

        <div className="rounded-md border border-border bg-muted p-3 text-sm">
          <div className="flex justify-between">
            <span className="text-muted-foreground">Total</span>
            <span suppressHydrationWarning className="font-mono text-foreground">
              {shown.price}
            </span>
          </div>
          <div className="mt-1 flex justify-between">
            <span className="text-muted-foreground">Rendered at</span>
            <span suppressHydrationWarning className="font-mono text-foreground">
              {shown.renderedAt}
            </span>
          </div>
        </div>

        <button
          onClick={() => setHydrated(true)}
          disabled={hydrated}
          className="mt-3 w-full rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground disabled:opacity-50"
        >
          {hydrated ? 'Hydrated' : 'Hydrate'}
        </button>

        {hydrated && (
          <div className="mt-3 space-y-1 text-xs">
            <p className="text-muted-foreground">
              Console warnings: <strong className="text-foreground">0</strong> — all suppressed
            </p>
            <p className="text-muted-foreground">
              Silently wrong on screen: <strong className="text-foreground">{silentlyWrong.length}</strong> field
              (Total)
            </p>
            <p className="text-muted-foreground">
              This client would have rendered <code>{CLIENT_RENDER.price}</code>, but the euro price is stuck as the
              server&apos;s dollars.
            </p>
          </div>
        )}
      </div>
      <p className="text-xs text-error mt-4">
        Blanket <code>suppressHydrationWarning</code> hid a real currency bug — the user sees the wrong total, and
        nothing warned anyone
      </p>
    </div>
  );
}
```

## Good — do this

`interactions-hydration-warning-good`

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

// The price is now formatted from one authoritative locale on both sides, so it matches.
const SERVER_HTML = { price: '1 234,50 €', renderedAt: '10:04:07' };
const CLIENT_RENDER = { price: '1 234,50 €', renderedAt: '13:22:41' };

export function HydrationWarningGood() {
  const [hydrated, setHydrated] = useState(false);

  // Deterministic fields hydrate normally: if they ever drift, React will tell us.
  const price = hydrated ? CLIENT_RENDER.price : SERVER_HTML.price;
  // The clock genuinely cannot match — it is a different moment. This is the one
  // place suppressHydrationWarning belongs, so the honest mismatch stays quiet.
  const renderedAt = hydrated ? CLIENT_RENDER.renderedAt : SERVER_HTML.renderedAt;

  return (
    <div className="w-full max-w-sm">
      <div className="bg-card border border-border rounded-lg p-4">
        <p className="text-xs text-muted-foreground mb-3">
          Only the clock is suppressed. The price is formatted the same way on both sides, so it hydrates under
          React&apos;s watch.
        </p>

        <div className="rounded-md border border-border bg-muted p-3 text-sm">
          <div className="flex justify-between">
            <span className="text-muted-foreground">Total</span>
            {/* No suppression: a drift here would surface as a warning, as it should. */}
            <span className="font-mono text-foreground">{price}</span>
          </div>
          <div className="mt-1 flex justify-between">
            <span className="text-muted-foreground">Rendered at</span>
            <span suppressHydrationWarning className="font-mono text-foreground">
              {renderedAt}
            </span>
          </div>
        </div>

        <button
          onClick={() => setHydrated(true)}
          disabled={hydrated}
          className="mt-3 w-full rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground disabled:opacity-50"
        >
          {hydrated ? 'Hydrated' : 'Hydrate'}
        </button>

        {hydrated && (
          <div className="mt-3 space-y-1 text-xs">
            <p className="text-muted-foreground">
              Suppressed fields: <strong className="text-foreground">1</strong> (the clock — genuinely unstable)
            </p>
            <p className="text-muted-foreground">
              Silently wrong on screen: <strong className="text-foreground">0</strong>
            </p>
            <p className="text-muted-foreground">
              The clock ticked forward. The total is the same €1 234,50 the user was quoted.
            </p>
          </div>
        )}
      </div>
      <p className="text-xs text-success mt-4">
        Suppression is scoped to the one value that cannot match — every other mismatch would still be reported
      </p>
    </div>
  );
}
```

## References

- [Vercel Web Interface Guidelines](https://github.com/vercel-labs/web-interface-guidelines/blob/main/command.md)
- [React: suppressHydrationWarning](https://react.dev/reference/react-dom/components/common)
- [React: hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot)
