I ran into this while building my site. The dreaded red error in the console:

Warning: Text content did not match. Server: "March 18, 2026" Client: "March 17, 2026"

Hydration mismatches. They're subtle, confusing, and can break your app in weird ways. Here's what I learned debugging them.

What Are Hydration Mismatches?

When Next.js renders a page, it first runs on the server and generates HTML. Then React "hydrates" that HTML on the client—it attaches event listeners and makes it interactive. The catch: React expects the client render to produce exactly the same HTML as the server.

When they don't match, React throws a hydration error. Sometimes it recovers gracefully. Sometimes your component just breaks.

Common Causes

1. Date/Time Formatting

This is the one that got me. My server runs in UTC. My browser runs in EST. When I render new Date().toLocaleDateString(), I get different strings:

// Server (UTC): "3/18/2026"
// Client (EST, late evening): "3/17/2026"
function PostDate({ date }: { date: string }) {
  return <time>{new Date(date).toLocaleDateString()}</time>
}

The server and client are literally in different time zones. Classic mismatch.

2. Browser-Only APIs

Using window, localStorage, or document during render:

// 💥 This explodes on the server (window is undefined)
// and causes hydration mismatch even with guards
function Theme() {
  const theme = window.localStorage.getItem('theme') || 'light'
  return <div className={theme}>...</div>
}

Even if you guard with typeof window !== 'undefined', you get a mismatch: the server renders with the fallback, the client renders with the real value.

3. Random Values or IDs

// 💥 Different ID every render
function Input() {
  const id = Math.random().toString(36).slice(2)
  return <input id={id} />
}

Server generates one ID, client generates another. Mismatch.

4. Browser Extensions

This one's sneaky. Browser extensions (password managers, ad blockers, translation tools) can modify your DOM after server HTML loads but before React hydrates. React sees the modified DOM, compares it to what it expected, and throws.

You can't fix this in your code—it's external. But you can recognize it: if the error only happens in certain browsers or only for certain users, extensions are probably the culprit.

How to Debug

The Error Message

React's error message tells you exactly what mismatched:

Warning: Text content did not match. Server: "X" Client: "Y"

Read it carefully. Is it a date? A dynamic value? A translation? The message usually points straight to the cause.

React DevTools

React DevTools highlights hydration issues. Components that failed hydration show warnings. This helps you narrow down which component is causing problems when the error message isn't clear.

View Source vs. Inspect

Compare "View Page Source" (the server HTML) with "Inspect Element" (the client DOM). If they differ in unexpected ways, you've found your mismatch.

Practical Solutions

1. useEffect for Browser-Only Code

Move browser-dependent logic into useEffect. It only runs on the client, after hydration:

function Theme() {
  const [theme, setTheme] = useState('light') // Same on server and client
 
  useEffect(() => {
    // Runs only on client, after hydration
    const saved = localStorage.getItem('theme')
    if (saved) setTheme(saved)
  }, [])
 
  return <div className={theme}>...</div>
}

Server renders with 'light'. Client hydrates with 'light'. Then useEffect updates to the saved theme. No mismatch.

2. Dynamic Imports with ssr: false

For components that simply can't render on the server:

import dynamic from 'next/dynamic'
 
const Map = dynamic(() => import('./Map'), { 
  ssr: false,
  loading: () => <div>Loading map...</div>
})
 
// Map only renders on client—no hydration issues
function LocationPage() {
  return <Map coordinates={[40.7, -74.0]} />
}

3. Consistent Date Formatting

Use UTC or a fixed timezone so server and client agree:

function PostDate({ date }: { date: string }) {
  // toISOString is always UTC—same on server and client
  const formatted = new Date(date).toISOString().split('T')[0]
  return <time>{formatted}</time>
}
 
// Or use a library with timezone support
import { formatInTimeZone } from 'date-fns-tz'
 
function PostDate({ date }: { date: string }) {
  const formatted = formatInTimeZone(
    new Date(date), 
    'America/New_York',  // Fixed timezone
    'MMMM d, yyyy'
  )
  return <time>{formatted}</time>
}

4. suppressHydrationWarning (Last Resort)

When you know content will differ and that's okay:

function Clock() {
  return (
    <time suppressHydrationWarning>
      {new Date().toLocaleTimeString()}
    </time>
  )
}

Use this sparingly. It silences the warning but doesn't fix the underlying issue. React will still replace the server content with client content—you're just hiding the error. Good for truly dynamic content like live clocks. Bad for everything else.

The Mental Model

Think of it this way: server render and first client render must be deterministic and identical. Anything that could differ between environments (time, randomness, browser APIs, user data) needs special handling:

  1. Defer it — use useEffect to run after hydration
  2. Skip it — use dynamic with ssr: false
  3. Stabilize it — use fixed timezones, seeded random, etc.
  4. Accept it — use suppressHydrationWarning (rarely)

Once I internalized this, hydration errors stopped being mysterious. They're just React telling you: "Hey, your server and client disagree. Pick one."

React to this post: