Skip to content

Fix: React Hydration Error — Text Content Does Not Match

FixDevs ·

Quick Answer

How to fix React hydration errors — server/client HTML mismatches, useEffect for client-only code, suppressHydrationWarning, dynamic content, and Next.js specific hydration issues.

The Error

React throws a hydration mismatch error in the browser console:

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>.

Or in Next.js:

Error: Text content does not match server-rendered HTML.
See more info here: https://nextjs.org/docs/messages/react-hydration-error

Or a more specific mismatch:

Warning: Prop `className` did not match. Server: "dark" Client: "light"
Warning: Expected server HTML to contain a matching <p> in <body>.

The page renders but the content flickers, or interactive elements don’t work correctly after the initial load.

Why This Happens

Hydration is the process where React attaches event listeners to server-rendered HTML. React expects the client-side render output to exactly match the server-rendered HTML. If they differ, React throws a hydration error and falls back to a full client-side re-render.

Common causes of mismatches:

  • Browser-only APIs in renderwindow, localStorage, navigator, document don’t exist on the server. Code that reads them returns different values server vs. client.
  • Random or time-dependent valuesMath.random(), Date.now(), new Date() produce different values on server and client.
  • Browser extensions — ad blockers, password managers, and translation extensions modify the DOM after server render but before hydration.
  • Invalid HTML nesting<p> inside <p>, <div> inside <p> — browsers auto-correct invalid HTML differently than React expects.
  • Conditional rendering based on client state — checking localStorage, cookies, or auth state that differs from the server’s assumption.
  • CSS-in-JS class names — some CSS-in-JS libraries generate non-deterministic class names.
  • Third-party scripts modifying the DOM before React hydrates.

Fix 1: Move Browser-Only Code to useEffect

Code that accesses browser APIs must run only on the client. useEffect runs only after the component mounts — never on the server:

// WRONG — window is undefined on server
function ThemeToggle() {
  const [theme, setTheme] = useState(
    window.localStorage.getItem('theme') || 'light'  // ReferenceError on server
  );
  return <button>{theme}</button>;
}

// WRONG — initial value differs server/client
function ThemeToggle() {
  const [theme, setTheme] = useState(
    localStorage.getItem('theme') || 'light'  // 'light' on server, stored value on client
  );
  // Hydration mismatch: server renders "light", client renders "dark"
  return <button>{theme}</button>;
}
// CORRECT — read browser APIs only after mount
function ThemeToggle() {
  const [theme, setTheme] = useState('light');  // Server-safe default

  useEffect(() => {
    // Runs only on client, after hydration completes
    const stored = localStorage.getItem('theme');
    if (stored) setTheme(stored);
  }, []);

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

Pattern — track whether component is mounted:

function ClientOnly({ children }: { children: React.ReactNode }) {
  const [mounted, setMounted] = useState(false);

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

  if (!mounted) return null;  // Server renders nothing — no mismatch
  return <>{children}</>;
}

// Use for browser-only components
<ClientOnly>
  <ThemeToggle />
  <BrowserStorageDisplay />
</ClientOnly>

Fix 2: Use dynamic() with ssr: false in Next.js

Next.js provides a built-in way to disable server-side rendering for specific components:

// next.js — disable SSR for a component
import dynamic from 'next/dynamic';

const ThemeToggle = dynamic(
  () => import('./ThemeToggle'),
  { ssr: false }  // Component only renders on client — no hydration mismatch
);

const Map = dynamic(
  () => import('./Map'),
  {
    ssr: false,
    loading: () => <div>Loading map...</div>,  // Shown server-side and during client load
  }
);

export default function Page() {
  return (
    <div>
      <ThemeToggle />  {/* Client-only — no SSR */}
      <Map />          {/* Client-only — no SSR */}
    </div>
  );
}

ssr: false trade-off — the component doesn’t render on the server at all, so search engines can’t index its content. Use it only for truly browser-only components (maps, charts, browser storage readers).

Fix 3: Use suppressHydrationWarning for Intentional Differences

Some differences between server and client are intentional and harmless — formatted dates, user-specific greetings. Suppress the warning on specific elements:

// Suppress warning on a specific element
function Timestamp({ date }: { date: Date }) {
  return (
    <time
      dateTime={date.toISOString()}
      suppressHydrationWarning  // ← Suppresses mismatch warning for this element only
    >
      {date.toLocaleString()}  {/* Format differs server/client based on locale */}
    </time>
  );
}

// Only suppresses the warning on the element it's applied to — not its children
<div suppressHydrationWarning>
  {new Date().toLocaleTimeString()}
</div>

Warning: suppressHydrationWarning hides the warning but doesn’t fix the mismatch. React still re-renders the element on the client. Use it only when the difference is cosmetic (time formatting, locale) and not functional. Don’t use it to silence real bugs.

Fix 4: Fix HTML Nesting Errors

Browsers silently fix invalid HTML nesting, producing a different DOM than React expects:

// WRONG — <p> cannot contain block-level elements
// Browser removes the inner <div>, causing a mismatch
<p>
  Some text
  <div>Block element</div>  {/* Invalid — <div> inside <p> */}
</p>

// WRONG — <ul> can only contain <li>
<ul>
  <div>Not a list item</div>  {/* Invalid */}
</ul>

// CORRECT
<div>
  <p>Some text</p>
  <div>Block element</div>
</div>

Common invalid nesting patterns in React:

// WRONG — interactive elements can't be nested
<button>
  <a href="/link">Click me</a>  {/* <a> inside <button> — invalid */}
</button>

// CORRECT — use one or the other
<a href="/link" role="button" className="btn">Click me</a>

// WRONG — <table> elements must follow strict nesting rules
<table>
  <div>Row wrapper</div>  {/* Invalid */}
</table>

// CORRECT
<table>
  <tbody>
    <tr>
      <td>Cell</td>
    </tr>
  </tbody>
</table>

Find HTML validation errors — paste your HTML into the W3C validator or use browser DevTools → Elements → look for any auto-corrected nesting.

Fix 5: Handle Date and Random Values

Values that change between renders (time, random numbers) cause hydration mismatches:

// WRONG — time differs between server render and client hydration
function Greeting() {
  const hour = new Date().getHours();
  const greeting = hour < 12 ? 'Good morning' : 'Good afternoon';
  return <p>{greeting}</p>;
  // Server: "Good morning" (UTC 9:00)
  // Client: "Good afternoon" (local 15:00) — mismatch
}

// CORRECT option 1 — render time-sensitive parts only on client
function Greeting() {
  const [greeting, setGreeting] = useState('Hello');  // Generic server-safe default

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

  return <p>{greeting}</p>;  // Server: "Hello", client: updates after mount
}

// CORRECT option 2 — pass the value as a prop from the server
// In Next.js App Router (Server Component):
export default function Page() {
  const hour = new Date().getHours();
  const greeting = hour < 12 ? 'Good morning' : 'Good afternoon';
  return <Greeting initialGreeting={greeting} />;
  // Server and client use the same greeting — no mismatch
}

Random IDs — use a stable ID generator:

// WRONG — Math.random() differs server/client
function Item() {
  const id = `item-${Math.random()}`;  // Different every render
  return <div id={id}>Content</div>;
}

// CORRECT — use React's useId() (React 18+)
import { useId } from 'react';

function Item() {
  const id = useId();  // Stable, same on server and client
  return <div id={id}>Content</div>;
}

// CORRECT — use a counter or UUID seeded from stable data
function Item({ index }: { index: number }) {
  return <div id={`item-${index}`}>Content</div>;
}

Fix 6: Handle Authentication and User State

Auth state often differs between server (not authenticated) and client (authenticated), causing hydration mismatches:

// WRONG — auth state differs server/client
function Header() {
  const isLoggedIn = checkAuthCookie();  // Returns false on server, true on client
  return (
    <nav>
      {isLoggedIn ? <UserMenu /> : <LoginButton />}
      {/* Server renders LoginButton, client renders UserMenu — mismatch */}
    </nav>
  );
}

Next.js App Router — pass auth state from server:

// app/layout.tsx (Server Component)
import { getServerSession } from 'next-auth';

export default async function Layout({ children }) {
  const session = await getServerSession();  // Server-side auth check

  return (
    <html>
      <body>
        <Header session={session} />  {/* Same data on server and client */}
        {children}
      </body>
    </html>
  );
}

// Header.tsx (Client Component)
'use client';
function Header({ session }: { session: Session | null }) {
  // Uses prop — no client-side auth check needed
  return (
    <nav>
      {session ? <UserMenu user={session.user} /> : <LoginButton />}
    </nav>
  );
}

Next.js Pages Router — use initial props:

export const getServerSideProps = async (context) => {
  const session = await getSession(context);
  return {
    props: { initialAuth: !!session },
  };
};

function Header({ initialAuth }: { initialAuth: boolean }) {
  // Use initialAuth for first render — then client state takes over
  const [isAuth, setIsAuth] = useState(initialAuth);

  useEffect(() => {
    // Sync with actual client-side auth state
    checkClientAuth().then(setIsAuth);
  }, []);

  return <nav>{isAuth ? <UserMenu /> : <LoginButton />}</nav>;
}

Fix 7: Debug Hydration Mismatches

React 18’s error message includes the expected vs. actual HTML. Use it to find the mismatch:

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

Hydration server mismatch:
  Server: <div class="dark">...</div>
  Client: <div class="light">...</div>

Add data- attributes to narrow down the mismatch:

// Add a unique marker to each suspiciously dynamic section
<div data-section="theme-provider">
  {children}
</div>
// If the error references this section, theme logic is causing the mismatch

Use React DevTools — in development, React DevTools shows a warning icon on components with hydration mismatches. Clicking shows the specific prop or text content that differed.

Check for browser extensions — disable all extensions and reload. If the error disappears, an extension is modifying the DOM. Common culprits: Grammarly (adds attributes to inputs), Google Translate (rewrites text nodes), ad blockers (remove elements).

React 18 Strict Mode in development double-invokes renders — this can expose bugs that cause hydration mismatches by making them more visible:

// main.tsx
<React.StrictMode>
  <App />
</React.StrictMode>
// Keep Strict Mode on — it helps catch bugs early

Still Not Working?

Hydration error from third-party scripts — scripts that modify the DOM (chat widgets, analytics) before React hydrates cause mismatches. Load them asynchronously after hydration:

// Load third-party scripts after hydration
useEffect(() => {
  const script = document.createElement('script');
  script.src = 'https://widget.example.com/chat.js';
  script.async = true;
  document.body.appendChild(script);
}, []);

Next.js <Script> component with strategy="lazyOnload" or strategy="afterInteractive" ensures scripts load after hydration.

CSS-in-JS hydration — libraries like styled-components require server-side collection of styles to avoid mismatches. Follow each library’s SSR documentation.

Locale-dependent formatting — if formatting differs by server locale vs. client locale, pass locale explicitly:

const formatted = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
}).format(amount);
// Specify locale explicitly — don't rely on system locale

For related React issues, see Fix: React useEffect Runs Twice and Fix: Next.js App Router Fetch Cache.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles