Fix Next.js Hydration Errors Using Claude Code

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

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

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

The page may flash, show incorrect content briefly, or lose interactive state. React cannot reconcile the server-rendered HTML with what the client tries to render.

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>; // Server-safe fallback
  }

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

What’s 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 and reconciling the server HTML with the client-side React tree.

Hydration fails when the server and client produce different output. React expects the HTML structure to match exactly. When it does not match, React must discard the server HTML and re-render from scratch, which causes a flash of content and loses the performance benefits of SSR.

Common causes of mismatches:

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

Step-by-Step Fix

Step 1: Identify the mismatch

Ask Claude Code to find the source:

I have a hydration error in my Next.js app. Search the codebase for
components that use browser-only APIs (window, document, localStorage,
navigator), date formatting, or Math.random() during render. Also check
for invalid HTML nesting.

Claude Code will scan your components and identify the specific files causing mismatches.

Step 2: Fix date/time mismatches

Dates are the number one cause of hydration errors:

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

// FIXED: Use suppressHydrationWarning for date display
function PostDate({ date }: { date: string }) {
  return (
    <time suppressHydrationWarning>
      {new Date(date).toLocaleDateString()}
    </time>
  );
}

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

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

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

Step 3: Fix browser API usage

Components that access window or document during render cause mismatches:

// BROKEN: window is undefined on server
function ResponsiveLayout({ children }: { children: React.ReactNode }) {
  const isMobile = window.innerWidth < 768; // Crashes on server

  return <div className={isMobile ? 'mobile' : 'desktop'}>{children}</div>;
}

// 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;
}

function ResponsiveLayout({ children }: { children: React.ReactNode }) {
  const isMobile = useIsMobile();
  return <div className={isMobile ? 'mobile' : 'desktop'}>{children}</div>;
}

Step 4: Fix localStorage/sessionStorage access

// BROKEN: localStorage not available during SSR
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState(
    localStorage.getItem('theme') || 'light' // Crashes on server
  );
  // ...
}

// FIXED: Read from storage in useEffect
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light'); // Server-safe default

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

  return <ThemeContext.Provider value={{ theme, setTheme }}>
    {children}
  </ThemeContext.Provider>;
}

Step 5: Fix invalid HTML nesting

React is strict about valid HTML. These cause hydration errors:

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

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

// BROKEN: Interactive elements inside interactive elements
<a href="/link">
  <button>Click me</button>
</a>

// FIXED: Use one or the other
<a href="/link" className="button-link">Click me</a>

Ask Claude Code to find all nesting violations:

Search my components for invalid HTML nesting that could cause
hydration errors. Check for: div inside p, p inside p, button inside a,
a inside a, interactive elements inside buttons.

Step 6: Fix third-party component issues

Some libraries render differently on server and client:

// Use dynamic import with ssr: false for client-only components
import dynamic from 'next/dynamic';

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

function LocationPage() {
  return (
    <div>
      <h1>Our Location</h1>
      <MapComponent />
    </div>
  );
}

Step 7: Debug with React DevTools

If the error source is still unclear, add a debug boundary:

'use client';

import { useEffect, useState } from 'react';

function HydrationDebug({ children }: { children: React.ReactNode }) {
  const [isClient, setIsClient] = useState(false);

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

  return (
    <div data-hydrated={isClient}>
      {children}
    </div>
  );
}

Prevention

Add these rules to your CLAUDE.md for Next.js projects:

## Next.js Hydration Rules
- 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 (no div inside p, no button inside a)
- Always test SSR output matches client output

**Written by Michael** — solo dev, Da Nang, Vietnam. 50K+ Chrome extension users. $500K+ on Upwork (100% Job Success). Runs 5 Claude Max subs in parallel. Built this site with autonomous agent fleets. [See what I'm building →](https://zovo.one)

I hit this exact error six months ago. Then I wrote a CLAUDE.md that tells Claude my stack, my conventions, and my error handling patterns. Haven't seen it since. I run 5 Claude Max subs, 16 Chrome extensions serving 50K users, and bill $500K+ on Upwork. These CLAUDE.md templates are what I actually use. Not theory — production configs. **[Grab the templates — $99 once, free forever →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-error&utm_campaign=claude-code-next-js-hydration-error-fix)** 47/500 founding spots. Price goes up when they're gone.