Fix Hydration Mismatch: "Hydration failed because the initial UI does not match what was rendered on the server"

intermediateโš›๏ธ React2026-03-19| React 18+, Next.js 13/14 (App Router or Pages Router), Node.js 18+

Error Message

Hydration failed because the initial UI does not match what was rendered on the server.
#react#hydration#ssr#nextjs

The Error

Error: Hydration failed because the initial UI does not match what was rendered on the server.

Warning: Expected server HTML to contain a matching <div> in <div>.
    at div
    at MyComponent

You pushed a deployment, everything looked fine locally, and now the client is throwing hydration errors in production โ€” or worse, silently rendering stale content. Here's how to track it down and kill it.

Why This Happens

React renders your component on the server and sends HTML to the browser. Then it "hydrates" โ€” attaching event handlers to the existing DOM. If the DOM React receives doesn't match what it would render itself, hydration blows up.

Nine times out of ten, the culprit is one of these:

  • Browser-only APIs (window, localStorage, navigator) accessed during render
  • Dates or timestamps rendered at slightly different moments on server vs client
  • Conditional rendering gated on typeof window !== 'undefined'
  • Browser extensions injecting nodes into the DOM (ad blockers, password managers, translation tools)
  • Invalid HTML nesting โ€” <div> inside <p>, or <p> inside <p>
  • Third-party components that aren't SSR-safe

Step-by-Step Fix

Step 1 โ€” Find the exact mismatch

Start the dev server. Open the browser console. Look for the full hydration error โ€” React 18 prints exactly what diffed:

pnpm dev
# or
npm run dev

Read the error output completely before touching anything. It names the component and the element. Nine times out of ten you can skip to the right file immediately.

Step 2 โ€” Fix browser-only code in render

Accessing window or localStorage at render time is the single most common cause. The server doesn't have these APIs. It crashes silently or returns undefined, and the output doesn't match the client.

Wrong:

// localStorage doesn't exist on the server โ€” this breaks SSR
export default function ThemeToggle() {
  const theme = localStorage.getItem('theme') || 'light';
  return <button>{theme}</button>;
}

Fixed โ€” move it into useEffect:

import { useState, useEffect } from 'react';

export default function ThemeToggle() {
  const [theme, setTheme] = useState('light'); // SSR-safe default

  useEffect(() => {
    const saved = localStorage.getItem('theme');
    if (saved) setTheme(saved);
  }, []);

  return <button>{theme}</button>;
}

useEffect never runs on the server. Both server and initial client render agree on 'light'. The client then syncs to the real value after hydration completes.

Step 3 โ€” Fix date/time rendering

Timestamps are a classic trap. The server renders at one moment, the client renders milliseconds later โ€” different output, hydration fails.

// Wrong โ€” new Date() on server != new Date() on client
<span>{new Date().toLocaleString()}</span>

// Fixed โ€” render the date on the client only
function ClientDate() {
  const [date, setDate] = useState('');
  useEffect(() => {
    setDate(new Date().toLocaleString());
  }, []);
  return <span>{date}</span>;
}

Empty string on first render, real timestamp after mount. No mismatch.

Step 4 โ€” Use dynamic imports for non-SSR-safe components

Some components simply can't run server-side โ€” map libraries, canvas-based charts, components that call browser APIs at import time. Don't fight it. Skip SSR entirely for those:

import dynamic from 'next/dynamic';

const MapComponent = dynamic(() => import('../components/Map'), {
  ssr: false,
  loading: () => <p>Loading map...</p>,
});

export default function Page() {
  return <MapComponent />;
}

The server sends the loading placeholder. The real component loads only after JavaScript runs in the browser. Clean separation, zero hydration conflict.

Step 5 โ€” Fix invalid HTML nesting

Browsers silently auto-correct bad HTML. React doesn't. That gap causes mismatches that look completely unrelated to the actual problem.

// Wrong โ€” block element inside <p> is invalid HTML
<p>
  <div>Some content</div>
</p>

// Fixed
<div>
  <div>Some content</div>
</div>

Run your page through the W3C Validator if you suspect structural issues. It catches nesting errors instantly.

Step 6 โ€” Handle conditional rendering correctly

Branching on typeof window during render is a guaranteed mismatch. The server sees undefined, the client sees the real window object โ€” they produce different HTML.

// Wrong
function Banner() {
  if (typeof window === 'undefined') return null;
  return <div>Client only banner</div>;
}

// Fixed โ€” use a mounted flag
function Banner() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  return <div>Client only banner</div>;
}

Step 7 โ€” Suppress unavoidable mismatches

Browser extensions are the wild card. Password managers, ad blockers, and translation tools inject DOM nodes you have zero control over. For those cases, suppress the warning at the container level:

<div suppressHydrationWarning={true}>
  {content}
</div>

A few caveats: this only suppresses one level deep, and it does nothing to fix actual bugs in your code. Use it only for legitimate external DOM modifications โ€” not as a way to silence errors you haven't debugged.

Verify the Fix

  • Open DevTools Console โ€” zero hydration warnings on page load means you're clean.
  • Disable JavaScript and reload. The server-rendered HTML should still be readable and structurally correct.
  • Run a production build:

pnpm build && pnpm start

    Production mode surfaces hydration mismatches that dev mode sometimes hides. Always test the production build before shipping.
  
  - Check React DevTools Profiler โ€” no unexpected remounts during the initial render tree.

## Staying Clean Long-Term

  - **Initialize state for the server, not the user.** The default value in `useState` must be safe for SSR. Anything user-specific โ€” theme preferences, auth state, locale โ€” belongs in `useEffect`.
  - **Audit third-party packages before installing.** Any library that touches `window` or `document` at import time needs `dynamic(..., { ssr: false })`. Check the package's README or GitHub issues โ€” someone has usually already run into it.
  - **Use cookies, not localStorage, for personalized SSR content.** Logged-in state, locale preferences, or A/B test variants rendered from `localStorage` will mismatch. Pass them server-side via cookies through `next/headers` instead.
  - **Test in a clean browser profile.** Your personal browser loaded with extensions will give false positives. Keep a bare Chrome profile specifically for hydration testing.

Related Error Notes