Fix "Hydration Failed" Error in Next.js — Server vs Client Mismatch Explained

The Next.js hydration error is one of the most confusing bugs in React — the page renders fine on the server, but React throws a mismatch error in the browser. This guide explains exactly why it happens and covers every common cause with working fixes.

SSR

root cause: server/client HTML differs at render time

8+

common causes covered with before/after fixes

Next.js

13/14/15 App & Pages Router — same root causes

useEffect

key pattern for browser-only operations

The error you're seeing

Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching element in the client tree. This error means React generated different HTML during hydration than what the server sent.
1

Why Hydration Errors Happen

The core problem

Next.js renders your page HTML on the server first, then "hydrates" it in the browser — attaching React event listeners to the existing server-rendered HTML. If React generates different HTML during hydration than what the server sent, it throws a mismatch error. The server and browser must produce identical HTML from the same component tree.

1

Server renders your component tree to HTML

Next.js runs your React components on the server and produces a static HTML string. This string is sent to the browser as the initial page response — it's what users see before JavaScript loads.

2

HTML is sent to the browser

The browser receives the pre-rendered HTML and displays it immediately (fast initial paint). At this point, the page looks right but has no interactivity — buttons don't work, state doesn't update.

3

React hydrates — re-runs components in the browser

React runs your component tree again in the browser to attach event handlers. During this re-run, React generates its own virtual DOM and compares it to the server HTML.

4

React compares browser output to server HTML

If the component produces identical HTML — great, hydration succeeds silently. If the output differs by even one character, React throws the hydration error.

5

Mismatch → hydration error thrown

React throws "Hydration failed" and displays the error overlay in development. In production, React tries to recover by re-rendering the entire tree client-side, causing a visible flash of content and performance degradation.

2

Cause 1 — Using Browser-Only APIs During SSR

window, localStorage, navigator don't exist on the server

If your component reads from window, localStorage, document, or navigator during the render phase, the server throws or returns nothing while the client renders with real values — a guaranteed mismatch.

localStorage during render — server crash

❌ Bad
// Reads localStorage during render — crashes on server
export default function ThemeButton() {
  const theme = localStorage.getItem('theme') || 'light'; // ❌ no localStorage on server
  return <button>{theme}</button>;
  // Server: ReferenceError: localStorage is not defined
  // Client: renders "dark" → mismatch
}

localStorage inside useEffect — runs only in browser

✅ Good
// Only read localStorage after mount
'use client';
import { useState, useEffect } from 'react';

export default function ThemeButton() {
  const [theme, setTheme] = useState('light'); // ✅ safe server default

  useEffect(() => {
    // useEffect only runs in the browser, never on the server
    setTheme(localStorage.getItem('theme') || 'light');
  }, []);

  return <button>{theme}</button>;
  // Server: renders "light" | Client: hydrates as "light", then updates after mount
}
3

Cause 2 — Date/Time Differences

Date during SSR render — timezone mismatch

❌ Bad
export default function Timestamp() {
  return <p>Now: {new Date().toLocaleString()}</p>;
  // ❌ Server: "1/15/2025, 10:00:00 AM" (UTC server time)
  // Client: "1/15/2025, 2:00:00 PM" (user's local timezone)
  // Different strings = hydration mismatch
}

Date inside useEffect — client-only rendering

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

export default function Timestamp() {
  const [time, setTime] = useState<string>(''); // empty on server

  useEffect(() => {
    setTime(new Date().toLocaleString()); // ✅ only rendered client-side
  }, []);

  if (!time) return <p>Loading time...</p>; // server shows placeholder

  return <p>Now: {time}</p>;
  // Server: "Loading time..." | Browser: shows actual local time after mount
}
4

Cause 3 — Invalid HTML Nesting

Invalid HTML — browser auto-corrects, React doesn't

❌ Bad
// <div> inside <p> is invalid HTML — browsers auto-correct it differently than React expects
export default function Card() {
  return (
    <p>
      <div>Content here</div>  {/* ❌ block element inside inline element */}
    </p>
  );
  // Browser moves <div> outside <p> automatically.
  // React sees a different DOM structure than it rendered → hydration mismatch
}

Valid HTML nesting — no auto-correction surprises

✅ Good
export default function Card() {
  return (
    <div>                    {/* ✅ div inside div is valid */}
      <div>Content here</div>
    </div>
  );
}

// Other common invalid nesting to avoid:
// <ul> inside <p>  → invalid
// <h1> inside <span> → invalid (block in inline)
// <table> without <tbody> → browsers add one automatically
5

Cause 4 — Browser Extensions

Test in incognito mode first

Extensions like Grammarly, password managers, LastPass, or ad blockers inject HTML attributes and DOM nodes (data-gramm, data-lt-installed) that React didn't render. This shows as hydration mismatches in development. Open Chrome in incognito (no extensions) and reload — if the error disappears, an extension is causing it, not your code.
6

Cause 5 — Math.random() or Unique IDs

Math.random() during render — different every time

❌ Bad
// Different random value on server vs client
export default function Badge() {
  const id = Math.random(); // ❌ server gets 0.123, client gets 0.456
  return <div id={`badge-${id}`}>New</div>;
  // Different id values = mismatch
}

useId() for stable server/client IDs

✅ Good
'use client';
import { useId } from 'react'; // React 18+ built-in stable ID generator

export default function Badge() {
  const id = useId(); // ✅ same value on server and client — stable
  return <div id={id}>New</div>;
}

// useId() generates deterministic IDs based on component position in the tree
// Same tree position = same ID on server and client
7

Cause 6 — Conditional Rendering Based on window

Checking window during render — always null on server

❌ Bad
// window is undefined on server → renders null → client renders something
export default function MobileMenu() {
  if (typeof window !== 'undefined' && window.innerWidth < 768) {
    return <nav>Mobile Nav</nav>;
  }
  return null;
  // Server: window is undefined → always returns null
  // Client: window.innerWidth may be 375 → returns <nav> → MISMATCH
}

Check window after mount with useEffect

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

export default function MobileMenu() {
  const [isMobile, setIsMobile] = useState(false); // false on server

  useEffect(() => {
    setIsMobile(window.innerWidth < 768); // ✅ runs only in browser
    // Optionally add a resize listener here
  }, []);

  if (!isMobile) return null; // server and initial client both return null
  return <nav>Mobile Nav</nav>; // shown after mount on mobile
}
8

Cause 7 — CSS-in-JS Class Name Mismatch

styled-components and emotion generate different class names

CSS-in-JS libraries that generate class names at runtime (styled-components, emotion) will produce different class names on the server vs client unless configured with SSR support. Configure the SSR provider to serialize styles on the server and inject them into the page.
jsxstyled-components SSR fix for Next.js App Router
// app/layout.tsx — wrap with styled-components registry
'use client';
import { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

export default function StyledComponentsRegistry({ children }) {
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== 'undefined') return <>{children}</>;

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}

// Wrap your root layout:
// export default function RootLayout({ children }) {
//   return <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
// }
9

The suppressHydrationWarning and dynamic() Escape Hatches

ItemApproachWhen to Use + Trade-offs
suppressHydrationWarningAdd to a specific elementUse only for intentional mismatches (timestamps, user content). Hides the warning without fixing the root cause.
dynamic({ ssr: false })Skip SSR for entire componentBest for browser-only components (maps, charts, WebGL). Shows loading state on server, renders component client-side only.
useEffect + stateDefer browser value to after mountCleanest fix. Server renders with safe default. Browser updates state after hydration. Small visual flash.
isClient stateBoolean: false on server, true after mountSimple pattern for "render X only on client" without dynamic import overhead.
jsxAll escape hatch patterns side by side
// Pattern 1: suppressHydrationWarning (for intentional, safe mismatches)
export default function LastSeen() {
  return (
    <time suppressHydrationWarning>
      {new Date().toLocaleString()}
    </time>
  );
  // ⚠️ Use only when the mismatch is harmless — don't hide real bugs
}

// Pattern 2: dynamic() with ssr: false (for browser-only components)
import dynamic from 'next/dynamic';
const MapComponent = dynamic(() => import('./Map'), {
  ssr: false,
  loading: () => <div className="h-64 bg-gray-100 animate-pulse" />,
});

// Pattern 3: isClient pattern (lightweight alternative to dynamic)
'use client';
import { useState, useEffect } from 'react';

export function ClientOnly({ children, fallback = null }) {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return isClient ? children : fallback;
}

// Usage:
// <ClientOnly fallback={<Skeleton />}>
//   <ComponentWithBrowserAPIs />
// </ClientOnly>

Frequently Asked Questions