Fix Next.js Hydration Errors Using Claude Code

The Problem

Your Next.js application throws a hydration 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>.

The page may flash, show incorrect content briefly, or lose interactive state.

Quick Fix

The most common cause is rendering browser-only values during SSR. Wrap dynamic content with a client-side check:

'use client';

import { useState, useEffect } from 'react';

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

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

  if (!mounted) {
    return <div>Welcome</div>;
  }

  return <div>Welcome, it is {new Date().toLocaleTimeString()}</div>;
}

What Is Happening

Next.js renders your React components on the server to produce HTML. The browser receives this HTML and displays it immediately. Then React "hydrates" the page by attaching event handlers. Hydration fails when the server and client produce different output. Common causes:

  1. Date/time rendering: Server and client are in different timezones
  2. Random values: Math.random() produces different values
  3. Browser APIs: window.innerWidth, localStorage, navigator.userAgent
  4. Invalid HTML nesting: <p> inside <p>, <div> inside <p>
  5. Third-party scripts: Browser extensions modifying the DOM

Step-by-Step Fix

Step 1: Fix date/time mismatches

// BROKEN: Server renders UTC, client renders local timezone
function PostDate({ date }: { date: string }) {
  return <time>{new Date(date).toLocaleDateString()}</time>;
}

// FIXED: Render stable format on server, enhance on client
function PostDate({ date }: { date: string }) {
  const [formatted, setFormatted] = useState(date);

  useEffect(() => {
    setFormatted(new Date(date).toLocaleDateString());
  }, [date]);

  return <time dateTime={date}>{formatted}</time>;
}

Step 2: Fix browser API usage

// BROKEN: window is undefined on server
const isMobile = window.innerWidth < 768;

// FIXED: Use a hook that handles SSR
function useIsMobile(): boolean {
  const [isMobile, setIsMobile] = useState(false);
  useEffect(() => {
    const check = () => setIsMobile(window.innerWidth < 768);
    check();
    window.addEventListener('resize', check);
    return () => window.removeEventListener('resize', check);
  }, []);
  return isMobile;
}

Step 3: Fix localStorage access

// FIXED: Read from storage in useEffect
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  useEffect(() => {
    const saved = localStorage.getItem('theme');
    if (saved) setTheme(saved);
  }, []);
  return <ThemeContext.Provider value={{ theme, setTheme }}>
    {children}
  </ThemeContext.Provider>;
}

Step 4: Fix invalid HTML nesting

<!-- BROKEN: div cannot be inside p -->
<p>Some text <div class="highlight">Highlighted</div></p>

<!-- FIXED: Use span or restructure -->
<p>Some text <span class="highlight">Highlighted</span></p>

Step 5: Use dynamic import for client-only components

import dynamic from 'next/dynamic';

const MapComponent = dynamic(() => import('./Map'), {
  ssr: false,
  loading: () => <div className="map-skeleton" />,
});

Prevention

Add these rules to your CLAUDE.md for Next.js projects: never access window, document, or localStorage during render. Use useEffect for browser-only values. Use suppressHydrationWarning only for dates/times. Use dynamic import with ssr: false for client-only libraries. Validate HTML nesting.

Paste your error into our Error Diagnostic for an instant fix.

Master Claude Code

Get lifetime access to all ClaudHQ tools, advanced workflows, and production-grade templates.

Get Lifetime Access

Written by the ClaudHQ team ยท Expert Claude Code guides and tools