Back to the blog
Reactยท Aug 7, 2026ยท 8 min read

Fixing React hydration errors: 'text content does not match server-rendered HTML'

Dates, random values, localStorage reads, and bad HTML nesting - the four causes behind almost every hydration mismatch in Next.js and other SSR React apps, with the fix for each.

#react#ssr#nextjs#hydration

Hydration errors - 'Text content does not match server-rendered HTML', 'Hydration failed because the initial UI does not match', or minified React error #418/#423 - all mean one thing: the HTML your server sent and the first client render disagreed. React expects them to be identical; when they aren't, it discards the server HTML and re-renders from scratch, which is both a console error and a real performance hit.

Cause 1: values that differ between server and client

// โŒ Server renders in UTC, client in the user's timezone
<span>{new Date().toLocaleTimeString()}</span>

// โŒ Different random value on each side
<div id={Math.random().toString(36)}>

// โœ… Stable ids: React's useId
const id = useId();

Dates, Math.random(), and locale formatting are the top offenders. For dates, render a stable representation (the ISO string, or a value computed once on the server and passed as a prop) and enhance to local time after mount. For ids, useId exists precisely so server and client agree.

Cause 2: reading browser-only state during render

// โŒ window doesn't exist on the server; ternary flips on client
const theme = typeof window !== 'undefined'
  ? localStorage.getItem('theme') : 'light';

// โœ… Two-pass render: server value first, real value after mount
const [theme, setTheme] = useState('light');
useEffect(() => {
  setTheme(localStorage.getItem('theme') ?? 'light');
}, []);

localStorage, window.innerWidth, matchMedia, cookies read client-side - anything the server can't see must not influence the first render. The two-pass pattern (render the server-safe default, correct it in an effect) trades a brief flash for correctness. For themes specifically, an inline script that sets a class on <html> before React loads avoids even the flash.

Cause 3: invalid HTML nesting

<p> inside <p>, <div> inside <p>, <tr> outside <tbody> - the browser silently rewrites invalid markup while parsing the server HTML, so the DOM React hydrates against no longer matches what your components describe. React 19's error messages now name the offending elements. The fix is always the same: make the nesting valid.

Cause 4: browser extensions (the one that isn't your fault)

  • Extensions like Grammarly and password managers inject attributes into the DOM before hydration - test in an incognito window before debugging your own code.
  • suppressHydrationWarning on the specific element is the escape hatch for values that legitimately differ (a timestamp, an ad slot) - it suppresses one element, not children, and shouldn't be sprinkled globally.
  • In production, minified error #418/#423 hides the diff - reproduce in development where React prints exactly which text mismatched.
Hydration errors are determinism bugs: some input to your render differs between the server and the browser. Name that input and the fix is mechanical.

Written by Appesto Engineering.