Fix: Hydration failed because the initial UI does not match what was rendered on the server (Next.js)

The Error

You load a page in Next.js and the console shows one of these errors:

Unhandled Runtime 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 <p>.

See more info here: https://nextjs.org/docs/messages/react-hydration-error

You may also see these related variants:

Warning: Text content did not match. Server: "2026-04-18" Client: "4/18/2026"
Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
Warning: Expected server HTML to contain a matching text node in <div>.

In development mode, Next.js shows a red error overlay with a full component stack trace. In production, the page silently falls back to full client-side rendering, which hurts performance and can cause a visible flash of content.

Why This Happens

Next.js pre-renders pages on the server (SSR or SSG). The server produces HTML and sends it to the browser. React then hydrates that HTML — it attaches event handlers and makes the page interactive. During hydration, React compares the server-rendered HTML with what the client-side render would produce. If they don’t match, React throws a hydration error.

The core rule is: the first client render must produce identical HTML to what the server rendered. Any difference — different text, different elements, different attributes, different nesting — is a mismatch.

Common causes include:

  1. The server and client have access to different data (e.g., window, localStorage, browser timezone).
  2. The HTML structure is invalid, and the browser “fixes” it before React can hydrate.
  3. Something modifies the DOM between server render and hydration (browser extensions, third-party scripts).
  4. Random or time-dependent values produce different output on server vs. client.

Each fix below addresses a specific cause.

Fix 1: Invalid HTML Nesting — <p> Inside <p>

The most common cause. HTML does not allow certain elements to be nested inside others. When the browser encounters invalid nesting, it silently restructures the DOM to fix it. React then sees a different tree than what it rendered on the server and throws a hydration error.

Broken code:

function BlogPost({ excerpt }) {
  return (
    <p>
      Summary: <p>{excerpt}</p>
    </p>
  );
}

The HTML spec says <p> cannot contain another <p>. The browser auto-closes the first <p> before opening the second one, producing a different DOM structure than what React expects.

Fix — use <span> or another inline element:

function BlogPost({ excerpt }) {
  return (
    <p>
      Summary: <span>{excerpt}</span>
    </p>
  );
}

Other common invalid nesting patterns that trigger this error:

// All of these are invalid and will cause hydration errors:
<p><div>...</div></p>           // div inside p
<p><h1>...</h1></p>             // heading inside p
<a><a href="/">...</a></a>      // anchor inside anchor
<table><div>...</div></table>   // div directly inside table

Use a validator or check the browser console for warnings about invalid nesting. If your component renders content from a CMS or markdown, make sure the HTML it produces follows valid nesting rules.

Fix 2: Browser Extensions Modifying the DOM

Browser extensions like Grammarly, LastPass, Google Translate, ad blockers, and dark mode extensions inject elements into the DOM after the server HTML is loaded but before React hydrates. React sees extra nodes it didn’t render and throws a mismatch error.

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

Fix — suppress the warning on the affected element:

<body suppressHydrationWarning>
  {children}
</body>

This tells React to skip the hydration check for that element’s direct content. It does not suppress warnings for the entire subtree, only for that specific node’s text content and attributes.

For elements you know extensions will modify (like <body> or <html>), this is a safe workaround. In Next.js App Router, you can set this in your root layout:

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

Fix 3: Using typeof window !== 'undefined' During Render

Checking for window during rendering produces different output on server (where window is undefined) and client (where it exists). This guarantees a mismatch.

Broken code:

function Greeting() {
  // Server renders "Server", client renders "Client" — mismatch
  const env = typeof window !== 'undefined' ? 'Client' : 'Server';
  return <p>Running on: {env}</p>;
}

Fix — use useEffect to detect the client:

import { useState, useEffect } from 'react';

function Greeting() {
  const [isClient, setIsClient] = useState(false);

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

  return <p>Running on: {isClient ? 'Client' : 'Server'}</p>;
}

The initial client render matches the server ('Server'). After hydration, useEffect runs and updates the state, triggering a second render that shows 'Client'. This two-pass approach avoids the mismatch because useEffect only runs on the client, after hydration is complete.

Fix 4: Date and Time Rendering Differences

The server and client may be in different timezones, or format dates differently depending on locale. new Date() returns the current time, which will always differ between SSR time and hydration time.

Broken code:

function Clock() {
  // Server renders "10:30:00 AM", client renders "10:30:05 AM" — mismatch
  return <p>{new Date().toLocaleTimeString()}</p>;
}

Fix — render dates only on the client:

import { useState, useEffect } from 'react';

function Clock() {
  const [time, setTime] = useState(null);

  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
    const interval = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  if (!time) return <p>Loading...</p>;
  return <p>{time}</p>;
}

For static dates that don’t change but might format differently across locales, use a fixed format:

// Instead of toLocaleDateString() which varies by environment
const formatted = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  timeZone: 'UTC',
}).format(date);

Specifying timeZone: 'UTC' ensures the server and client produce the same string regardless of their system timezone.

Fix 5: Using useEffect for Client-Only Code

Any code that depends on browser APIs (window, document, navigator, localStorage) must run inside useEffect or an event handler — never during the initial render. This is the general pattern that fixes most hydration issues.

Broken code:

function ScreenSize() {
  // window doesn't exist on the server — crashes or produces different output
  const width = window.innerWidth;
  return <p>Width: {width}px</p>;
}

Fix — defer to useEffect:

import { useState, useEffect } from 'react';

function ScreenSize() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    setWidth(window.innerWidth);
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <p>Width: {width}px</p>;
}

The server and initial client render both show 0. After hydration, useEffect reads the actual window width and updates the state. If accessing properties on the width value later causes issues, check for TypeError: Cannot read properties of undefined which is common when working with potentially missing values.

Fix 6: Dynamic Import with ssr: false

For components that fundamentally cannot run on the server (e.g., they use canvas, WebGL, window extensively), use Next.js dynamic imports with SSR disabled.

Fix:

import dynamic from 'next/dynamic';

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

function ContactPage() {
  return (
    <div>
      <h1>Find Us</h1>
      <MapComponent />
    </div>
  );
}

With ssr: false, Next.js doesn’t render the component on the server at all. The server outputs the loading fallback, and the client loads and renders the component after hydration. No mismatch is possible because the server HTML and initial client render both show the fallback.

This is the right approach for charting libraries, map widgets, WYSIWYG editors, and anything that imports a module that references window or document at the top level. If the import itself fails, you might also see a Module not found: Can’t resolve error — make sure the package is installed.

Fix 7: Using suppressHydrationWarning

For content that you know will differ between server and client and that’s acceptable, React provides suppressHydrationWarning as an escape hatch.

function Timestamp() {
  return (
    <time dateTime={new Date().toISOString()} suppressHydrationWarning>
      {new Date().toLocaleString()}
    </time>
  );
}

Important caveats:

  • It only suppresses the warning for one level deep. It won’t silence mismatches in child elements.
  • It does not fix the mismatch — it just tells React to accept it. The client content will overwrite the server content.
  • Use it sparingly. If you’re putting suppressHydrationWarning on many elements, you likely have a deeper architectural problem.

Legitimate use cases: timestamps, analytics IDs, A/B test variants, and content that intentionally differs per user.

Fix 8: localStorage and sessionStorage Access During SSR

localStorage and sessionStorage don’t exist on the server. Reading from them during render produces different content on server vs. client.

Broken code:

function ThemeWrapper({ children }) {
  // Server: theme is null (localStorage doesn't exist)
  // Client: theme is "dark"
  // Mismatch!
  const theme = localStorage.getItem('theme') || 'light';
  return <div className={theme}>{children}</div>;
}

Fix — read storage in useEffect:

import { useState, useEffect } from 'react';

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

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

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

Both server and initial client render use 'light'. After hydration, useEffect reads the stored preference and updates. This causes a brief flash of the default theme, but avoids the hydration error.

To eliminate the flash, consider setting the theme via a <script> tag in the <head> that runs before React hydrates. Next.js libraries like next-themes handle this pattern for you.

Fix 9: Third-Party Scripts Modifying the DOM

Scripts loaded via <script> tags (analytics, chat widgets, consent banners) can insert DOM nodes before React hydrates. React then sees a DOM tree that doesn’t match what it rendered.

Broken code:

// pages/_document.js
<Head>
  <script src="https://third-party.com/widget.js" />
</Head>

If that script inserts a <div> into the <body> synchronously, the DOM is already modified when React tries to hydrate.

Fix — use the Next.js <Script> component with an appropriate strategy:

import Script from 'next/script';

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://third-party.com/widget.js"
        strategy="afterInteractive"
      />
    </>
  );
}

strategy="afterInteractive" loads the script after hydration completes, so it can’t interfere with the DOM matching process. For scripts that must run earlier, use strategy="beforeInteractive" but make sure they don’t modify the DOM React manages.

Fix 10: Math.random() and Crypto Differences

Random values generated during render will always differ between server and client.

Broken code:

function UniqueId() {
  // Server generates "id-0.7231" — client generates "id-0.1892" — mismatch
  const id = `id-${Math.random().toFixed(4)}`;
  return <div id={id}>Content</div>;
}

Fix — generate IDs with useId (React 18+) or in useEffect:

import { useId } from 'react';

function UniqueId() {
  const id = useId();
  return <div id={id}>Content</div>;
}

useId generates the same ID on server and client for the same component position in the tree. It’s specifically designed for this use case.

If you need random values for non-ID purposes, defer them to useEffect:

const [randomValue, setRandomValue] = useState(0);
useEffect(() => {
  setRandomValue(Math.random());
}, []);

The same applies to crypto.randomUUID() or any other non-deterministic function. If you’re running into type errors when working with the generated values, see TypeScript: Type is not assignable for handling type narrowing correctly.

Fix 11: Conditional Rendering Based on Authentication State

Auth state is often unknown on the server (no cookies, no session) but known on the client. Rendering different UI based on auth state during the initial render causes a mismatch.

Broken code:

function Header() {
  const { user } = useAuth(); // Returns null on server, user object on client

  return (
    <nav>
      {user ? (
        <span>Welcome, {user.name}</span>
      ) : (
        <a href="/login">Sign In</a>
      )}
    </nav>
  );
}

If the auth provider resolves differently on server vs. client, the rendered HTML won’t match.

Fix — show a loading/default state until auth resolves on the client:

function Header() {
  const { user, isLoading } = useAuth();
  const [mounted, setMounted] = useState(false);

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

  return (
    <nav>
      {!mounted || isLoading ? (
        <span>Loading...</span>
      ) : user ? (
        <span>Welcome, {user.name}</span>
      ) : (
        <a href="/login">Sign In</a>
      )}
    </nav>
  );
}

Both server and initial client render show Loading.... After hydration, useEffect sets mounted to true, and the component re-renders with the actual auth state. This pattern works for any state that’s only available on the client — user preferences, feature flags, A/B test buckets.

If the auth hook itself throws errors about hooks being called in the wrong order, see React Hook is called conditionally for rules about hook ordering.

Fix 12: React Portal Issues

React portals (createPortal) render content into a DOM node that exists outside the React root. If the target node doesn’t exist on the server or has a different structure, hydration fails.

Broken code:

import { createPortal } from 'react-dom';

function Modal({ children }) {
  // document doesn't exist on the server — this crashes or mismatches
  return createPortal(
    <div className="modal">{children}</div>,
    document.getElementById('modal-root')
  );
}

Fix — only render the portal on the client:

import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';

function Modal({ children }) {
  const [mounted, setMounted] = useState(false);

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

  if (!mounted) return null;

  const target = document.getElementById('modal-root');
  if (!target) return null;

  return createPortal(
    <div className="modal">{children}</div>,
    target
  );
}

Make sure the modal-root element exists in your HTML. In Next.js App Router, add it to your root layout:

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

The portal renders null during SSR and the first client render, then mounts into the target element after hydration. This avoids any mismatch because the server output and initial client output both produce nothing for the modal.

Still Not Working?

Check the Full Error Stack Trace

Next.js 14+ includes detailed hydration error diffs in development mode. The error overlay shows the exact elements that mismatched — server HTML on one side, client HTML on the other. Read the diff carefully. It usually points directly at the problem element.

If the error doesn’t point to a clear cause, temporarily remove half your page content. If the error disappears, it’s in the removed half. Keep narrowing until you find the offending component. This is faster than reading through hundreds of lines of JSX.

Check for Whitespace and Text Node Differences

Extra whitespace, newlines, or invisible characters can cause mismatches. This is especially common with template literals and pre-formatted text:

// Server might render different whitespace than the client
<pre>{`
  Line 1
  Line 2
`}</pre>

Normalize your whitespace or use trim() to ensure consistency.

Watch for Stale Cached Server Responses

If you’re using ISR (Incremental Static Regeneration) or aggressive caching, the cached HTML may include outdated content that no longer matches what the client component produces. Clear your .next cache and rebuild:

rm -rf .next
npm run build
npm run start

Avoid Rendering User-Agent-Dependent Content

If you render different HTML based on navigator.userAgent or req.headers['user-agent'], make sure the server and client use the same value. Better yet, don’t branch rendering logic on the user agent — use CSS media queries or feature detection in useEffect instead.

Check for Multiple React Versions

If your project has duplicate React installations (e.g., a dependency bundles its own React), the server and client may use different React instances, causing hydration to fail entirely. Run npm ls react to check for duplicates. This can also cause Too many re-renders and other confusing errors. If the duplicates come from module resolution issues, also check for Module not found: Can’t resolve errors in your build output.


Related: Fix: Too many re-renders | Fix: React Hook is called conditionally | Fix: TypeError: Cannot read properties of undefined | Fix: Type is not assignable | Fix: Module not found: Can’t resolve

Related Articles