React Next.js Fix 9 min read

React Hydration Error? Fix Text Content Mismatches

Troubleshoot Text content does not match server-rendered HTML and Hydration failed because the initial UI does not match across React, Next.js App Router, and Pages Router.

Common causes and fixes

1. Non-deterministic values in render (Date.now, Math.random)

The single most common cause. If a component calls Date.now(), new Date().toLocaleString(), or Math.random() directly in the render body, the server produces one value at build/request time and the client produces a different value milliseconds later during hydration.

Warning: Text content did not match. Server: "Loaded at 2:41:02 PM"
Client: "Loaded at 2:41:03 PM"

Fix — defer to useEffect: render a static value first, then swap it in after mount:

function Timestamp() {
  const [now, setNow] = useState(null);

  useEffect(() => {
    setNow(new Date());
  }, []);

  // Renders identically on server and first client pass
  if (!now) return <span>Loading…</span>;

  return <span>{now.toLocaleTimeString()}</span>;
}

2. Browser-only APIs accessed during render

window, localStorage, and navigator do not exist on the server. Reading them in the render body either throws window is not defined during SSR, or if you guard it with typeof window !== 'undefined', produces different output on server vs. client -- which is itself a hydration mismatch.

// WRONG -- server renders "server", client renders actual width
function Width() {
  const width = typeof window !== 'undefined' ? window.innerWidth : 0;
  return <span>{width}</span>;
}

// CORRECT -- both passes render 0, real value arrives after mount
function Width() {
  const [width, setWidth] = useState(0);
  useEffect(() => {
    setWidth(window.innerWidth);
  }, []);
  return <span>{width}</span>;
}

Same rule applies to localStorage.getItem() for theme or auth state -- read it in useEffect, not in the component body.

3. Invalid HTML nesting

The browser's HTML parser silently rewrites invalid nesting -- a <div> inside a <p>, or a <table> without a <tbody> -- before React ever hydrates. React's virtual DOM expects the original structure, sees the browser-corrected structure instead, and throws.

// WRONG -- <div> is not allowed inside <p>, browser closes <p> early
<p>
  Summary text
  <div className="badge">New</div>
</p>

// CORRECT
<div>
  Summary text
  <div className="badge">New</div>
</div>

Other frequent offenders: <a> nested inside <a>, <button> inside <button>, and <ul> with non-<li> children.

4. Browser extensions injecting DOM before hydration

Password managers, Grammarly, and ad blockers inject attributes or elements into the DOM (like data-gramm or wrapper <span> tags) before React hydrates. This is not your bug -- verify it by testing in an incognito window with extensions disabled. If the error disappears, use suppressHydrationWarning on the affected element (usually <body> or a form input):

// app/layout.tsx (Next.js App Router)
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body suppressHydrationWarning>{children}</body>
    </html>
  );
}

5. Locale and timezone differences

If your server runs in UTC (typical for Vercel/AWS Lambda) but the browser runs in the user's local timezone, toLocaleDateString() and Intl.DateTimeFormat can produce different strings on each side. Fix by explicitly passing a fixed timezone and locale rather than relying on the runtime default:

// WRONG -- uses server/browser's ambient timezone, which can differ
new Date(ts).toLocaleDateString();

// CORRECT -- deterministic on server and client
new Intl.DateTimeFormat('en-US', {
  timeZone: 'UTC',
  dateStyle: 'medium',
}).format(new Date(ts));

6. Disabling SSR for fully client-only components

For components that inherently depend on browser APIs (charting libraries, map widgets, rich text editors), the cleanest fix is to skip server rendering entirely with Next.js dynamic import:

import dynamic from 'next/dynamic';

const ChartWidget = dynamic(() => import('./ChartWidget'), {
  ssr: false,
  loading: () => <div className="chart-skeleton" />,
});

App Router vs. Pages Router: in the Pages Router, ssr: false works directly in any component. In the App Router, ssr: false is only allowed inside Client Components (files with 'use client') -- calling it from a Server Component throws a build error, so wrap it in its own client-marked file.

7. Debugging with React DevTools

React 18+ collapses hydration errors into a single generic message in production. To see exactly which DOM node mismatched, run in development mode -- the console prints a diff with + Client and - Server lines pointing at the exact element and component stack. React DevTools also flags components that re-rendered immediately after hydration, which is a strong signal something client-only slipped into the initial render.

Hydration failed because the server rendered HTML didn't match the client.
  <Header>
    <nav>
+     <span>Logged in as guest</span>
-     <span>Logged in as maxim</span>

Astro comparison: Astro islands avoid this class of bug by default -- a component only hydrates client-side JavaScript when you add a directive like client:load (hydrate immediately) or client:visible (hydrate on scroll into view). Everything else stays static HTML with no diffing step, so there is nothing to mismatch.

🔔

Prismix tracks status for the AI services your app depends on

While you're debugging your frontend, don't let an OpenAI or Vercel outage waste more of your time -- free status alerts, no credit card.

FAQ

What does "Text content does not match server-rendered HTML" mean?

React renders once on the server and once on the client during hydration. If the two outputs differ -- different text, different attributes, different structure -- React throws this warning because it cannot safely reuse the server-generated markup and event listeners.

How do I fix a Next.js hydration mismatch caused by a timestamp?

Move the timestamp into useEffect so it renders after hydration, or add suppressHydrationWarning if the mismatch is cosmetic and expected. Never call Date.now() or Math.random() directly in render.

Why does invalid HTML nesting cause hydration errors?

Browsers auto-correct invalid nesting (like <div> inside <p>) before React hydrates, so the DOM React finds does not match the tree it expects to attach to.

How is Astro hydration different from Next.js hydration?

Astro ships zero JavaScript by default and only hydrates components you mark with a client directive like client:load or client:visible. Next.js hydrates the full React tree by default, so any server/client divergence anywhere in that tree can trigger a mismatch.

Monitor related services