If you have built anything with the Next.js App Router, you have almost certainly met this red wall of text. Your page renders, then React throws away the server HTML and complains that the markup it rendered on the client does not match what came from the server. Let's understand why it happens and walk through every reliable fix.
The Error
Hydration failed because the initial UI does not match what was rendered on the server. Warning: Text content did not match. Server: "..." Client: "..."
Why This Happens
Next.js renders your component to HTML on the server, sends it to the browser, then React 'hydrates' it — attaching event listeners and reconciling its own render against the existing DOM. Hydration fails whenever the first client render produces different markup than the server did. The most common triggers are:
- Using
Date.now(),new Date(),Math.random()orcrypto.randomUUID()directly in render. - Reading
window,localStorage, ornavigatorduring the first render. - Invalid HTML nesting — a
<div>inside a<p>, or a<p>inside another<p>. - Browser extensions (Grammarly, dark-mode injectors) mutating the DOM before hydration.
- Locale or timezone-dependent formatting that differs between server and client.
Fix 1: Defer Browser-Only Values to useEffect
Anything that can only be known in the browser must not be rendered on the first pass. Render a stable placeholder, then update inside useEffect (which never runs on the server):
"use client";
import { useEffect, useState } from "react";
export function Clock() {
const [time, setTime] = useState<string | null>(null);
// Runs only in the browser, after hydration is complete.
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
// First render matches the server (null), so no mismatch.
return <span>{time ?? "--:--:--"}</span>;
}Fix 2: Use suppressHydrationWarning for Unavoidable Mismatches
For a single element whose content is legitimately time-dependent, React lets you silence the warning for that node only. Use it sparingly:
<time suppressHydrationWarning>{new Date().getFullYear()}</time>Fix 3: Dynamically Import Client-Only Components
If an entire component depends on the browser (charts, maps, editors), skip SSR for it entirely with next/dynamic:
import dynamic from "next/dynamic";
const MapView = dynamic(() => import("./MapView"), { ssr: false });Fix 4: Check Your HTML Nesting
The browser silently 'repairs' invalid HTML, which changes the DOM out from under React. A classic offender is putting a block element inside a paragraph. Replace the outer <p> with a <div> and the mismatch disappears.
Quick Checklist
1) No Date/Math.random in render. 2) No window/localStorage on first render. 3) Valid HTML nesting. 4) Test in an incognito window to rule out extensions.
Hydration errors look scary but almost always reduce to *the server and client disagreed about the first render*. Make that first render deterministic and the error is gone for good.
