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

The Error

You load a Next.js or React SSR page and hit one of these errors in the console:

React 18+:

Hydration failed because the initial UI does not match what was rendered on the server.
Text content does not match server-rendered HTML.
There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

React 17 and earlier:

Expected server HTML to contain a matching <div> in <div>.

All of these mean the same thing: the HTML rendered on the server does not match what React tried to render on the client during hydration. React compares the server-rendered DOM against the component tree it builds client-side. When they don’t match, hydration fails.

Why This Happens

When you use server-side rendering (SSR) — which Next.js does by default — React renders your components to HTML on the server and sends that HTML to the browser. The browser displays it immediately. Then React’s JavaScript loads, and React “hydrates” the page: it walks through the existing DOM and attaches event handlers, state, and interactivity without rebuilding the DOM from scratch.

Hydration assumes the DOM already matches what React would render. If even a single element or text node differs between server and client, React detects a mismatch. (If your environment variables are undefined on the client but not the server, that’s a different problem — see Fix: process.env.VARIABLE_NAME is undefined.) In React 18, it logs the error and falls back to full client-side rendering, which is slower and defeats the purpose of SSR. In React 17, it attempts to patch the DOM but can produce broken UIs.

The mismatch happens because something in your component produces different output depending on whether it runs on the server or in the browser.

Fix

1. Browser Extensions Modifying the DOM

Browser extensions like Grammarly, password managers, ad blockers, and translation tools inject elements into the DOM after the server HTML loads but before React hydrates. React sees elements it didn’t render and throws a hydration error.

This is the most frustrating cause because your code is correct — an extension is altering the page.

How to confirm: Open your page in an incognito window with all extensions disabled. If the error disappears, an extension is the cause.

Fix — add suppressHydrationWarning to the affected elements:

<body suppressHydrationWarning>
  {children}
</body>

For Next.js App Router, apply it in your root layout:

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

suppressHydrationWarning tells React to skip the hydration check for that element. It only applies one level deep — it won’t suppress warnings for child components. Use it sparingly and only on elements you know are targets for extension injection (typically <body> or <html>).

Important: This prop silences the warning. It does not fix the underlying mismatch. Only use it when the mismatch is caused by something outside your control.

2. Date, Time, or Locale-Dependent Rendering

The server and browser run in different environments. new Date() returns a different timestamp on the server than on the client. toLocaleString() may format differently depending on the server’s locale settings versus the browser’s.

Broken code:

export default function Greeting() {
  const hour = new Date().getHours();
  return <p>{hour < 12 ? 'Good morning' : 'Good afternoon'}</p>;
}

The server renders “Good morning” at 9 AM server time. The client hydrates at 3 PM user time. Mismatch.

Fix — render time-dependent content only on the client with useEffect:

"use client"; // Next.js App Router

import { useState, useEffect } from 'react';

export default function Greeting() {
  const [greeting, setGreeting] = useState('Hello');

  useEffect(() => {
    const hour = new Date().getHours();
    setGreeting(hour < 12 ? 'Good morning' : 'Good afternoon');
  }, []);

  return <p>{greeting}</p>;
}

The server and client both render “Hello” initially. After hydration, useEffect runs client-side and updates the greeting. No mismatch.

For timestamps and formatted dates, the same pattern applies:

"use client";

import { useState, useEffect } from 'react';

export default function Timestamp() {
  const [time, setTime] = useState('');

  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
  }, []);

  // Renders empty string on server, then fills in on client
  return <time>{time}</time>;
}

Tip: If you need to display a date that was already computed (like a post’s publish date), toISOString() produces identical output on server and client because it always uses UTC:

// Safe — deterministic format, no locale differences
const dateStr = post.createdAt.toISOString().split('T')[0]; // "2026-03-14"

But new Date() itself still returns a different value on server versus client, so new Date().toISOString() will differ if the two calls happen at different times. Only use this for pre-existing date values, not for “current time.”

3. Invalid HTML Nesting

Browsers auto-correct invalid HTML before React gets a chance to hydrate. When the browser “fixes” the DOM, it no longer matches what React expects.

Common invalid nesting patterns:

// ❌ <p> cannot contain <div>
<p>
  <div>This is wrong</div>
</p>

// ❌ <p> cannot contain <p>
<p>
  <p>Nested paragraph</p>
</p>

// ❌ <a> cannot contain <a>
<a href="/one">
  <a href="/two">Nested link</a>
</a>

// ❌ <table> requires specific children
<table>
  <div>Should be inside <tr> and <td></div>
</table>

When the browser encounters <p><div>, it closes the <p> before the <div>, creating two sibling elements instead of a nested structure. React expects the nested structure it rendered on the server. The DOM doesn’t match.

Fix — use valid HTML nesting:

// ✅ Use <div> or <span> as the outer wrapper
<div>
  <div>This works</div>
</div>

// ✅ Use <span> inside <p> for inline content
<p>
  <span>Inline content is fine</span>
</p>

Watch out for component composition. The nesting might not be obvious when components wrap each other:

// This looks fine, but if TextBlock renders a <p> internally,
// you end up with <p><p>...</p></p>
<p>
  <TextBlock content="Hello" />
</p>

Check what your child components actually render. A component that renders a <div> inside a <p> parent causes the same issue.

Next.js dev mode (React 18.3+) reports exactly which tags are mismatched. Look at the console for messages like Expected server HTML to contain a matching <div> in <p> — the tag names tell you where to look.

4. Conditional Rendering with typeof window !== 'undefined'

This is a common pattern developers use to detect the browser environment. But it causes hydration mismatches because the condition is false on the server and true on the client.

Broken code:

export default function Navigation() {
  // Server: false, Client: true → different output
  const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;

  return (
    <nav>
      {isMobile ? <MobileMenu /> : <DesktopMenu />}
    </nav>
  );
}

The server always renders <DesktopMenu />. The client may render <MobileMenu />. Mismatch.

Fix — use useEffect and state to detect client-side values:

"use client";

import { useState, useEffect } from 'react';

export default function Navigation() {
  const [isMobile, setIsMobile] = useState(false);

  useEffect(() => {
    setIsMobile(window.innerWidth < 768);

    const handleResize = () => setIsMobile(window.innerWidth < 768);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <nav>
      {isMobile ? <MobileMenu /> : <DesktopMenu />}
    </nav>
  );
}

Both server and client render <DesktopMenu /> initially. After hydration, useEffect checks the actual viewport width and updates the state. This triggers a re-render with the correct component. No mismatch.

For a reusable pattern, create a custom hook:

import { useState, useEffect } from 'react';

function useIsMounted() {
  const [mounted, setMounted] = useState(false);

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

  return mounted;
}

Then use it to guard any client-only rendering:

"use client";

export default function ClientOnlyFeature() {
  const mounted = useIsMounted();

  if (!mounted) return null; // or a placeholder

  return <div>{window.innerWidth}px wide</div>;
}

5. Using Browser-Only APIs During SSR (localStorage, window, navigator)

Accessing localStorage, sessionStorage, window, document, or navigator during rendering crashes on the server (where these APIs don’t exist) or produces different output.

Broken code:

export default function ThemeProvider({ children }) {
  // Crashes on server: localStorage is not defined
  const theme = localStorage.getItem('theme') || 'light';

  return <div className={theme}>{children}</div>;
}

Fix — read browser APIs inside useEffect:

"use client";

import { useState, useEffect } from 'react';

export default function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

  return <div className={theme}>{children}</div>;
}

Server and client both render with 'light' initially. After hydration, the client reads from localStorage and updates if needed.

If you initialize useState with a function that accesses browser APIs, that function runs during SSR too:

// ❌ This function runs on the server
const [value, setValue] = useState(() => localStorage.getItem('key'));

// ✅ Use a safe default, then read in useEffect
const [value, setValue] = useState(null);
useEffect(() => {
  setValue(localStorage.getItem('key'));
}, []);

6. Third-Party Components Not SSR-Compatible

Some component libraries (charts, maps, rich text editors, media players) use browser APIs internally and are not designed for server-side rendering. They access window, document, or the Canvas API during initialization, causing errors or rendering differences.

Fix — use Next.js dynamic with ssr: false:

import dynamic from 'next/dynamic';

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

export default function Dashboard() {
  return (
    <div>
      <h1>Analytics</h1>
      <Chart data={data} />
    </div>
  );
}

ssr: false tells Next.js to skip rendering this component on the server entirely. The server outputs the loading fallback. The client loads and renders the full component. No mismatch because the component never ran on the server.

For React without Next.js, use lazy loading with a client-only wrapper:

import { lazy, Suspense, useEffect, useState } from 'react';

const Chart = lazy(() => import('./Chart'));

function ClientOnly({ children, fallback = null }) {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return fallback;
  return <Suspense fallback={fallback}>{children}</Suspense>;
}

export default function Dashboard() {
  return (
    <ClientOnly fallback={<p>Loading chart...</p>}>
      <Chart data={data} />
    </ClientOnly>
  );
}

Common libraries that need ssr: false:

  • react-quill, draft-js, tiptap (rich text editors)
  • chart.js, recharts with certain plugins, apexcharts
  • react-leaflet, mapbox-gl
  • react-player
  • Any library whose docs mention “client-side only”

Still Not Working?

Use React DevTools to Debug the Mismatch

React 18.3+ shows detailed hydration mismatch diffs in the console during development. The error message includes the expected (server) HTML and the actual (client) HTML, making it easier to spot the difference.

If you’re on an older version of React, upgrade to at least 18.3 for better error messages:

npm install react@latest react-dom@latest

React DevTools (the browser extension) also highlights hydration errors in the component tree. Look for red warning indicators on affected components. If your components are re-rendering excessively after fixing hydration issues, see Fix: Too many re-renders.

Check Next.js Dev Mode Output

Next.js development mode (next dev) surfaces hydration errors with component stack traces. The error overlay shows exactly which component caused the mismatch and often includes a diff of the expected versus actual content.

Read the full error carefully. It usually points directly to the problematic element. For example:

Expected server HTML to contain a matching <div> in <p>.

This tells you there’s a <div> inside a <p> — an invalid nesting issue (see Fix 3).

Clear the .next Cache

Sometimes stale build artifacts cause phantom hydration errors that don’t reflect your actual code.

rm -rf .next
npm run dev

On Windows:

Remove-Item -Recurse -Force .next
npm run dev

If the error persists after clearing the cache, it’s a real mismatch in your code, not a caching issue.

Check for Mismatched Dependencies

Ensure your react and react-dom versions match exactly. Mismatched versions can cause subtle hydration bugs:

npm ls react react-dom

Both should show the same version. If they differ, align them:

npm install react@latest react-dom@latest

Search for Direct DOM Manipulation

If you or a library are using document.getElementById(), element.innerHTML, or jQuery to modify the DOM outside of React, those changes won’t be reflected in React’s virtual DOM. React will see a mismatch during hydration.

Search your codebase for direct DOM manipulation:

  • document.getElementById
  • document.querySelector
  • .innerHTML
  • .appendChild
  • .removeChild

If you need to manipulate the DOM directly, do it inside useEffect (which only runs after hydration) and use useRef to get a reference to the element.


Related: Fix: TypeError: Cannot read properties of undefined

Related Articles