Introduction

React Server Component (RSC) hydration mismatch errors occur when the HTML generated on the server does not match what React renders on the client during the hydration process, causing UI inconsistencies, lost interactivity, or complete render failures. RSCs render exclusively on the server and send HTML to the client, where React "hydrates" the static HTML into an interactive application. Hydration mismatches happen when client-side rendering produces different output than server rendering due to: timestamps or random values differing, browser APIs (window, localStorage) unavailable on server, conditional rendering based on client-only state, date formatting with different timezones, window size or media queries affecting layout, user agent detection causing different output, or improper use of 'use client' directive. Common causes include rendering Date.now() or Math.random() on both server and client, accessing localStorage during initial render, using window.innerWidth for conditional rendering, authentication state differing between server and client, third-party libraries expecting browser APIs during SSR, and suspense boundaries not properly configured for streaming. The fix requires understanding the RSC lifecycle, proper server/client component boundaries, strategies for client-only APIs, and debugging tools for hydration errors. This guide provides production-proven patterns for Next.js App Router, Remix, and other RSC-enabled frameworks.

Symptoms

  • Console warning: "Hydration failed because the server and client HTML differ"
  • React logs: "Text content does not match server-rendered HTML"
  • Interactive elements stop working after page load
  • Event handlers not attaching to hydrated elements
  • UI flickers or jumps after hydration completes
  • Components re-render unexpectedly after initial mount
  • Suspense boundaries show fallback indefinitely
  • Streaming HTML stops mid-stream
  • "Error building source: Hydration error" in console
  • Components appear static (non-interactive) despite client JavaScript loaded
  • Next.js console: "Warning: Prop className mismatch"
  • Production builds work differently than development

Common Causes

  • Date/time rendering differs between server and client (timezone)
  • Math.random() or unique ID generation during render
  • localStorage/sessionStorage accessed during SSR
  • window/document APIs used in server components
  • Conditional rendering based on client-only state
  • User agent detection producing different HTML
  • Window size/media query based rendering
  • Authentication state sync issues
  • Third-party libraries initializing during SSR
  • Improper 'use client' directive placement

Step-by-Step Fix

### 1. Diagnose hydration mismatches

Enable React hydration logging:

```javascript // In Next.js, enable React debug logging // .env.local NEXT_REACT_DEBUGGING=1

// Or in next.config.js module.exports = { reactStrictMode: true, // Enable detailed hydration errors experimental: { reactRoot: true, } }

// In development, React logs detailed mismatch info // Look for console warnings like: // "Warning: Text content did not match. Server: '123' Client: '456'" ```

Use browser DevTools to inspect mismatch:

```javascript // Chrome DevTools > Elements tab // React DevTools shows component tree

// When hydration fails: // 1. Open Elements tab // 2. Find highlighted element with warning // 3. Compare server HTML vs client DOM

// Server HTML (view source): <div class="container">Server Time: 12:00:00</div>

// Client DOM (after hydration): <div class="container">Client Time: 12:00:05</div>

// Mismatch detected: Text content differs ```

Identify server vs client components:

```javascript // In Next.js App Router: // app/ // ├── page.js // Server Component (default) // ├── layout.js // Server Component (default) // ├── components/ // │ ├── ServerComp.js // Server Component // │ └── ClientComp.js // Client Component (needs directive)

// Check component type: // No directive = Server Component // 'use client' at top = Client Component

// Server Component (runs only on server) export default function ServerComp() { // Can access filesystem, database, env vars const data = await db.query(); return <div>{data}</div>; }

// Client Component (runs on client after hydration) 'use client';

export default function ClientComp() { // Can access browser APIs const [width, setWidth] = useState(window.innerWidth); return <div>{width}</div>; } ```

### 2. Fix date/time hydration issues

Avoid rendering dynamic dates on server:

```javascript // WRONG: Date differs between server and client render export default function ServerComp() { const now = new Date(); return <div>Current time: {now.toLocaleTimeString()}</div>; // Server: "Current time: 12:00:00" // Client: "Current time: 12:00:05" (5 seconds later) // Hydration mismatch! }

// CORRECT: Render date only on client 'use client';

export default function ClientComp() { const [now, setNow] = useState(null);

useEffect(() => { setNow(new Date()); // Only runs on client }, []);

if (!now) return <div>Loading...</div>; // Show placeholder during SSR

return <div>Current time: {now.toLocaleTimeString()}</div>; }

// Or use a stable server timestamp export default function ServerComp() { const serverTime = new Date().toISOString(); // Stable string return ( <div> <ServerTime time={serverTime} /> </div> ); } ```

Handle timezone differences:

```javascript // WRONG: Timezone causes mismatch export default function EventPage({ event }) { const eventDate = new Date(event.date); return <div>Event starts: {eventDate.toLocaleString()}</div>; // Server (UTC): "Event starts: 2024-01-15 10:00:00" // Client (EST): "Event starts: 2024-01-15 05:00:00" // Mismatch! }

// CORRECT: Use UTC or pass timezone explicitly export default function EventPage({ event }) { const eventDate = new Date(event.date); // Use UTC for consistent rendering const utcString = eventDate.toISOString(); return <div>Event starts: {utcString}</div>; }

// Or render on client with user's timezone 'use client';

export default function EventPage({ event }) { const [formattedDate, setFormattedDate] = useState('');

useEffect(() => { const eventDate = new Date(event.date); setFormattedDate(eventDate.toLocaleString()); // Client timezone }, [event.date]);

return <div>Event starts: {formattedDate}</div>; } ```

### 3. Fix random value and ID mismatches

Avoid Math.random() during render:

```javascript // WRONG: Random ID differs server vs client export default function Component() { const id = Math.random().toString(36).substr(2, 9); return <div id={id}>Content</div>; // Server: id="abc123" // Client: id="xyz789" // Hydration mismatch! }

// CORRECT: Generate ID on mount (client only) 'use client';

export default function Component() { const [id, setId] = useState('');

useEffect(() => { setId(Math.random().toString(36).substr(2, 9)); }, []);

return <div id={id || 'loading'}>Content</div>; }

// Or use useId hook (React 18+) 'use client';

import { useId } from 'react';

export default function Component() { const id = useId(); // Consistent between server and client! return ( <div> <label htmlFor={${id}-input}>Name</label> <input id={${id}-input} type="text" /> </div> ); } ```

Handle UUID generation properly:

```javascript // WRONG: Generate UUID during render import { v4 as uuidv4 } from 'uuid';

export default function List({ items }) { return ( <ul> {items.map(item => ( <li key={uuidv4()}>{item.name}</li> // New ID every render! ))} </ul> ); }

// CORRECT: Use stable IDs from data export default function List({ items }) { return ( <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> // Stable ID ))} </ul> ); }

// Or generate IDs on server before passing to component export default async function Page() { const items = await db.items.findMany(); // Items already have IDs from database return <List items={items} />; } ```

### 4. Fix localStorage/sessionStorage access

Avoid storage access during SSR:

```javascript // WRONG: localStorage unavailable on server export default function UserProfile() { const theme = localStorage.getItem('theme') || 'light'; return <div className={theme}>Content</div>; // Server: localStorage undefined, theme = 'light' // Client: localStorage available, theme = 'dark' (user preference) // Hydration mismatch! }

// CORRECT: Access storage only on client 'use client';

export default function UserProfile() { const [theme, setTheme] = useState('light'); // Default matches server

useEffect(() => { const savedTheme = localStorage.getItem('theme') || 'light'; setTheme(savedTheme); // Update after mount }, []);

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

// Or use a storage wrapper with SSR safety function useLocalStorage(key, defaultValue) { const [value, setValue] = useState(defaultValue); const [mounted, setMounted] = useState(false);

useEffect(() => { setMounted(true); const stored = localStorage.getItem(key); if (stored !== null) { setValue(stored); } }, [key]);

const setStoredValue = (newValue) => { setValue(newValue); if (mounted) { localStorage.setItem(key, newValue); } };

return [value, setStoredValue]; }

// Usage 'use client';

export default function UserProfile() { const [theme, setTheme] = useLocalStorage('theme', 'light'); return <div className={theme}>Content</div>; } ```

Handle session state properly:

```javascript // WRONG: Session data differs server vs client export default function Dashboard() { const session = getSession(); // Custom function const isLoggedIn = session?.user !== undefined; return <div>{isLoggedIn ? 'Logged in' : 'Not logged in'}</div>; // Server: Session from cookies = logged in // Client: Session not synced yet = not logged in // Mismatch! }

// CORRECT: Pass session from server export default async function Dashboard() { const session = await getServerSession(); // Server-side return <ClientDashboard session={session} />; }

// Client component receives session as prop 'use client';

export default function ClientDashboard({ session }) { // Session is serialized, consistent between server and client return <div>{session?.user ? 'Logged in' : 'Not logged in'}</div>; } ```

### 5. Fix window/document API usage

Avoid browser APIs in server components:

```javascript // WRONG: window unavailable on server export default function ResponsiveLayout() { const width = window.innerWidth; // Error on server! return <div>{width > 768 ? 'Desktop' : 'Mobile'}</div>; // Server: window is undefined, crashes // Or if polyfilled, width may differ from client }

// CORRECT: Use client component for browser APIs 'use client';

export default function ResponsiveLayout() { const [width, setWidth] = useState(0);

useEffect(() => { setWidth(window.innerWidth); // Only runs on client

const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize);

return () => window.removeEventListener('resize', handleResize); }, []);

return <div>{width > 768 ? 'Desktop' : 'Mobile'}</div>; }

// Or use CSS for responsive design (no JS needed) export default function ResponsiveLayout() { return ( <div> <div className="mobile-only">Mobile</div> <div className="desktop-only">Desktop</div> </div> ); }

// In CSS: // @media (max-width: 768px) { // .desktop-only { display: none; } // } // @media (min-width: 769px) { // .mobile-only { display: none; } // } ```

Handle document access safely:

```javascript // WRONG: document unavailable on server export default function ScrollPosition() { const scrollY = document.documentElement.scrollTop; return <div>Scrolled: {scrollY}px</div>; // Server: document undefined }

// CORRECT: Access document in useEffect 'use client';

export default function ScrollPosition() { const [scrollY, setScrollY] = useState(0);

useEffect(() => { const handleScroll = () => { setScrollY(document.documentElement.scrollTop); };

handleScroll(); // Initial value window.addEventListener('scroll', handleScroll);

return () => window.removeEventListener('scroll', handleScroll); }, []);

return <div>Scrolled: {scrollY}px</div>; }

// Or use React ref for direct DOM access 'use client';

import { useRef, useEffect } from 'react';

export default function ElementPosition() { const ref = useRef(null); const [top, setTop] = useState(0);

useEffect(() => { if (ref.current) { const rect = ref.current.getBoundingClientRect(); setTop(rect.top); } }, []);

return <div ref={ref}>Top: {top}px</div>; } ```

### 6. Fix conditional rendering mismatches

Handle authentication state consistently:

```javascript // WRONG: Auth state may differ server vs client export default function UserMenu() { const { user } = useAuth(); // Client-side auth hook return ( <div> {user ? <LoggedInMenu user={user} /> : <LoginButton />} </div> ); // Server: No auth state, shows LoginButton // Client: Auth loaded, shows LoggedInMenu // Hydration mismatch! }

// CORRECT: Pass auth state from server export default async function Layout() { const session = await getServerSession(); return <ClientLayout session={session} />; }

'use client';

export function ClientLayout({ session }) { return ( <div> {session?.user ? ( <LoggedInMenu user={session.user} /> ) : ( <LoginButton /> )} </div> ); } ```

Handle feature flags properly:

```javascript // WRONG: Feature flag may differ server vs client export default function NewFeature() { const enabled = getFeatureFlag('new-ui'); // May differ! return enabled ? <NewUI /> : <OldUI />; // Server: Flag from API = true // Client: Cached flag = false // Mismatch! }

// CORRECT: Fetch flags on server, pass as props export default async function Page() { const flags = await getFeatureFlags(); // Server-side return <ClientPage flags={flags} />; }

'use client';

export function ClientPage({ flags }) { // Flags are serialized, consistent return flags.newUi ? <NewUI /> : <OldUI />; }

// Or use static feature flags at build time // next.config.js const featureFlags = { newUi: process.env.FEATURE_NEW_UI === 'true', };

export default function Page() { return featureFlags.newUi ? <NewUI /> : <OldUI />; } ```

### 7. Fix suspense boundary issues

Configure suspense for streaming:

```javascript // WRONG: Suspense boundary misconfigured export default function Page() { return ( <Suspense fallback={<Loading />}> <HeavyComponent /> </Suspense> ); // May cause hydration issues if fallback HTML differs }

// CORRECT: Ensure fallback is identical server and client import { Suspense } from 'react';

export default function Page() { return ( <Suspense fallback={ <div className="skeleton-loader"> <div className="skeleton-line" /> <div className="skeleton-line" /> <div className="skeleton-line" /> </div> } > <HeavyComponent /> </Suspense> ); }

// HeavyComponent can be async (Server Component) async function HeavyComponent() { const data = await fetchData(); return <div>{data}</div>; } ```

Handle nested suspense boundaries:

```javascript // Multiple suspense boundaries for streaming export default function Dashboard() { return ( <div> <Suspense fallback={<HeaderSkeleton />}> <Header /> </Suspense>

<Suspense fallback={<SidebarSkeleton />}> <Sidebar /> </Suspense>

<Suspense fallback={<MainSkeleton />}> <MainContent /> </Suspense> </div> ); }

// Each section streams when ready // Fallback must be identical on server and client ```

### 8. Fix third-party library hydration issues

Handle analytics scripts:

```javascript // WRONG: Analytics may run on server import { Analytics } from 'analytics-library';

export default function Layout({ children }) { Analytics.initialize('TRACKING_ID'); // Runs on server! return <div>{children}</div>; }

// CORRECT: Initialize analytics on client only 'use client';

import { useEffect } from 'react'; import { Analytics } from 'analytics-library';

export default function Layout({ children }) { useEffect(() => { Analytics.initialize('TRACKING_ID'); // Client only }, []);

return <div>{children}</div>; }

// Or use next/script for Next.js import Script from 'next/script';

export default function Layout({ children }) { return ( <div> {children} <Script src="https://analytics.com/script.js" strategy="afterInteractive" // Loads after page interactive /> </div> ); } ```

Handle UI libraries with SSR:

```javascript // WRONG: UI library expects browser APIs import { Tooltip } from 'ui-library';

export default function HelpText() { return <Tooltip content="Help">?</Tooltip>; // Tooltip may use document/popper.js which fails on server }

// CORRECT: Render UI library in client component 'use client';

import { Tooltip } from 'ui-library';

export default function HelpText() { return <Tooltip content="Help">?</Tooltip>; }

// Or disable SSR for specific component import dynamic from 'next/dynamic';

const TooltipNoSSR = dynamic( () => import('ui-library').then(mod => mod.Tooltip), { ssr: false } // Skip server rendering );

export default function HelpText() { return <TooltipNoSSR content="Help">?</Tooltip>; } ```

### 9. Debug RSC hydration with React DevTools

Use React DevTools component tree:

```javascript // Install React DevTools extension

// In DevTools > Components tab: // - Server Components shown in purple // - Client Components shown in blue // - Hover to see props and hooks

// When hydration fails: // 1. Find component with warning icon // 2. Inspect props passed from server // 3. Check if client re-renders with different props

// Common patterns: // - Server passes timestamp, client recalculates // - Server passes serialized data, client deserializes differently // - Conditional rendering based on client-only state ```

Enable React hydration logging:

```javascript // In React 18+, hydration errors include detailed info // Console shows: // "Hydration failed because the server and client HTML differ" // "Server: 'Hello World'" // "Client: 'Hello Universe'"

// To get more details: // Create custom error handler

// app/error.js (Next.js) 'use client';

export default function Error({ error, reset }) { useEffect(() => { console.error('Hydration error:', error); }, [error]);

return <div>Error occurred</div>; } ```

Use useSyncExternalStore for external data:

```javascript // WRONG: Subscribe to external store during render export default function StoreComponent() { const value = store.getValue(); // May differ server vs client return <div>{value}</div>; }

// CORRECT: Use useSyncExternalStore (React 18+) 'use client';

import { useSyncExternalStore } from 'react';

export default function StoreComponent() { const value = useSyncExternalStore( store.subscribe, // Subscribe function store.getSnapshot, // Server snapshot store.getClientSnapshot // Client snapshot (optional) );

return <div>{value}</div>; }

// Ensures consistent value between server and client ```

### 10. Implement SSR-safe patterns

Create SSR-safe hooks:

```javascript // hooks/useIsClient.js import { useState, useEffect } from 'react';

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

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

return isClient; }

// hooks/useWindowSize.js export function useWindowSize() { const [size, setSize] = useState({ width: 0, height: 0 }); const isClient = useIsClient();

useEffect(() => { if (!isClient) return;

const updateSize = () => { setSize({ width: window.innerWidth, height: window.innerHeight, }); };

updateSize(); window.addEventListener('resize', updateSize); return () => window.removeEventListener('resize', updateSize); }, [isClient]);

return size; }

// Usage 'use client';

import { useWindowSize } from './hooks/useWindowSize';

export default function ResponsiveComponent() { const { width } = useWindowSize();

if (width === 0) return <div>Loading...</div>; // SSR placeholder

return <div>{width > 768 ? 'Desktop' : 'Mobile'}</div>; } ```

Create SSR-safe components:

```javascript // components/ClientOnly.js 'use client';

import { useEffect, useState } from 'react';

export default function ClientOnly({ children, fallback = null }) { const [mounted, setMounted] = useState(false);

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

if (!mounted) return fallback;

return children; }

// Usage import ClientOnly from './components/ClientOnly';

export default function Page() { return ( <div> <ServerContent /> <ClientOnly fallback={<div>Loading...</div>}> <ClientContent /> </ClientOnly> </div> ); } ```

Prevention

  • Always use 'use client' directive for browser API access
  • Pass data from server to client via props, don't fetch on both
  • Use useId() hook for unique IDs instead of Math.random()
  • Render dates/times consistently (UTC or client-only)
  • Defer localStorage access until useEffect
  • Use CSS for responsive layouts instead of window.innerWidth
  • Configure suspense boundaries with identical fallbacks
  • Test hydration in both development and production builds
  • **Hydration failed**: Server and client HTML differ
  • **Text content mismatch**: Dynamic content differs between renders
  • **Prop className mismatch**: CSS classes differ server vs client
  • **useEffect runs twice**: React Strict Mode double invocation
  • **Component not interactive**: Hydration failed, events not attached