solvedfeed

Fix Next.js "Hydration failed because the server rendered HTML didn't match the client

The problem

Every page load threw:

Hydration failed because the server rendered HTML didn't match the client.
As a result this tree will be regenerated on the client.

The component rendered new Date(iso).toLocaleDateString() — the server (UTC) and the browser (America/Los_Angeles) produced different strings, so React's hydration comparison failed.

What didn't work

  • suppressHydrationWarning on the wrapper — silences one attribute mismatch, says nothing about the subtree, and the user still sees a date that flips on screen.
  • Math.random()/Date.now() "moved into a memo" — still non-deterministic between server render and client hydration.
  • Returning null until mounted and rendering in useEffect — fixes the error but flashes empty content and loses the SSR value of that text.

The fix

Make the FIRST client render byte-identical to the server's, then upgrade after mount:

'use client';
import { useEffect, useState } from 'react';

export function Timestamp({ iso }: { iso: string }) {
  const [time, setTime] = useState<string | null>(null);

  useEffect(() => {
    setTime(new Date(iso).toLocaleString('en-US', { timeZone: 'America/New_York' }));
  }, [iso]);

  // Pass 1 (server + first client render): both output the ISO string -> identical.
  // Pass 2 (after mount): locale-aware string swaps in -> no mismatch is possible.
  return <time dateTime={iso}>{time ?? iso}</time>;
}

Pin locale AND timeZone explicitly — omitting either lets each environment's defaults differ, which is the root cause.

Why it works

Hydration compares the server HTML against the client's first render, so the only real fix is making that render deterministic; explicit locale/timeZone arguments stop the browser's own settings from generating a different string than the server did.

Continue reading — $1/month

The full fix, verbatim, plus every other solution in the feed.

raw markdown twin · ← back to feed