← Back to the library
FrontendApplied · 7 min

Hydration fails or the page flickers after SSR

“The console reports that server-rendered HTML did not match the client, or production logs show React error #418, and parts of the page re-render, flicker, or lose event handlers.”

ERROR CODES YOU MAY SEE

A code is a clue. Use its meaning and surrounding evidence to narrow the cause.

418React minified errors · library

Hydration failed because the server-rendered HTML did not match the first client render; React regenerates that tree on the client.

Production builds show only the number. Reproduce in a development build to see the diff, then look for time, locale, randomness, browser-only branches, invalid HTML nesting, or HTML rewritten by extensions or a CDN.

Official reference for 418 (opens in new tab)Code definition reviewed

THE PRINCIPLE

Hydration is not a re-render; it adopts existing HTML. Anything that depends on the clock, locale, time zone, randomness, or the browser belongs after hydration or must be passed in from the server.

FIRST MOVES

  1. Reproduce in a development build to get the diff; production only reports the error number.
  2. Look for Date.now(), new Date(), toLocaleString(), Math.random(), typeof window branches, and localStorage or matchMedia reads in render.
  3. Replace hand-rolled id counters with useId, and keep the component tree identical on both sides.
  4. Validate the HTML: no <div> or <p> inside <p>, no nested <a> or <button>; the browser parser rewrites invalid nesting.
  5. Rule out extensions, iOS format detection, and CDN rewriting by testing in a clean profile against the raw origin response.
  6. Defer genuinely client-only UI with useEffect, next/dynamic with ssr: false, or Astro client:only; use suppressHydrationWarning only for a single unavoidable text node.

TOOLS: THEN → NOW

Global id counters and typeof window checksuseId and post-hydration effects
Page-wide SSR for every widgetIslands or per-component client-only rendering where SSR adds nothing

PATTERN SNAPSHOT

// Server and first client render agree: fixed locale and time zone.
const fmt = new Intl.DateTimeFormat("en-US", { timeZone: "UTC", dateStyle: "medium" });

function LocalDate({ iso }: { iso: string }) {
  const [text, setText] = useState(() => fmt.format(new Date(iso)));
  // Switch to the viewer's locale only after hydration.
  useEffect(() => { setText(new Date(iso).toLocaleDateString()); }, [iso]);
  return <time dateTime={iso}>{text}</time>;
}

const id = useId(); // stable across server and client, unlike a counter

CLOSE THE AI. EXPLAIN THIS.

Why does an incrementing id counter break hydration even when the server and client run the same code?

SOURCES

Guide reviewed