What is a hydration error in Next.js and why does it happen?
A hydration error fires when the HTML the server rendered doesn't match what the client renders on first run. Causes: `new Date()`/`Math.random()`/`window.*` in render, locale/timezone mismatches, `typeof window` branches, third-party scripts mutating DOM before hydration, browser extensions modifying HTML. Fixes: client-only wrappers, suppressHydrationWarning for unavoidable cases, deterministic IDs (`useId`), check the actual DOM diff in the error message.
Next.js (and any SSR React app) prints a hydration mismatch when the client-rendered DOM differs from the server-rendered HTML.
The error
Hydration failed because the server rendered HTML didn't match the client.
React shows you the exact element where divergence occurred.
Why it happens
The server renders <App/> once, sends HTML. The client mounts, runs <App/> again, expects identical output. If the two trees disagree, hydration fails — and React falls back to a client-side re-render, which is slow and bad for UX.
Common causes
1. Time/randomness in render.
function Footer() {
return <p>© {new Date().getFullYear()}</p>; // year is the same most of the time
}
function Stamp() {
return <p>{new Date().toLocaleTimeString()}</p>; // server time ≠ client time
}Server renders at 10:00:00, client hydrates at 10:00:01 → mismatch.
2. Locale / timezone differences.
<p>{new Date().toLocaleDateString()}</p>
// Server: 'en-US' (Vercel default)
// Client: 'fr-FR' → mismatch3. typeof window !== 'undefined' branches.
return <div>{typeof window === 'undefined' ? 'server' : 'client'}</div>;Server renders 'server', client renders 'client'.
4. Browser-only APIs.
const isMobile = window.innerWidth < 768; // crashes on server, or different value5. Third-party scripts that mutate DOM before hydration.
Cookie banners, A/B test SDKs, browser extensions (LastPass, Grammarly) that inject attributes/elements into the DOM.
6. Conditional rendering based on session/auth state.
If the server doesn't know whether the user is logged in (cookie-based auth not read SSR), it might render the logged-out tree while client renders logged-in.
7. Class component constructor side-effects that vary by environment.
Fixes
A. Move client-only logic to useEffect.
function Stamp() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <p>{time ?? '—'}</p>;
}Server renders —; client updates after hydration.
B. ClientOnly wrapper.
function ClientOnly({ children }: { children: ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []);
return mounted ? <>{children}</> : null;
}Use sparingly — the wrapped tree renders blank on the server.
C. useId for stable IDs.
const id = useId(); // same on server and client
<input id={id} />Replaces crypto.randomUUID() for form-element IDs.
D. suppressHydrationWarning.
For unavoidable single-element mismatches:
<time suppressHydrationWarning>{new Date().toISOString()}</time>Use very sparingly — silences the warning but doesn't fix the underlying flicker.
E. Resolve session/auth on the server.
In Next.js App Router, read cookies in a Server Component:
import { cookies } from 'next/headers';
export default async function Page() {
const session = await getSession(cookies());
return <Layout user={session?.user ?? null} />;
}F. Defer third-party scripts.
<Script src="..." strategy="afterInteractive" />Loads after hydration so the script can't pre-mutate.
Browser extensions
LastPass, Grammarly, etc. add attributes like data-lpignore to inputs. They modify the DOM before React hydrates. Solutions:
- Add
suppressHydrationWarningto inputs (it suppresses just that element). - Detect via
useEffectand re-render.
Reading the error
Next.js prints both trees. Find the first difference:
- Server: <div className="foo">A</div>
+ Client: <div className="foo">B</div>The diverging value is the culprit. Trace upward to find the source.
Performance impact
A hydration mismatch causes React to discard the SSR DOM and re-render the entire subtree client-side. LCP and TTI take a hit. Repeated mismatches mean you've lost most of the SSR benefit.
Senior framing
Hydration errors are usually a sign that the component depends on something the server doesn't know — time, viewport, browser APIs, session cookies. The fix is to push that decision either entirely to the server (read cookies, resolve session) or entirely to the client (useEffect + suspense). Avoid 'maybe-server-maybe-client' branches in render.
Follow-up questions
- •How would you handle authentication-dependent UI in SSR?
- •What's the difference between useId and crypto.randomUUID() for SSR?
- •When is suppressHydrationWarning the right tool?
Common mistakes
- •Branching on typeof window in render.
- •Using new Date()/Math.random() in render output.
- •Silencing all hydration warnings instead of finding the source.
Performance considerations
- •A mismatch forces a full client re-render of the affected subtree. LCP / TTI both regress. Frequent mismatches mean SSR isn't earning its keep — switch the offending tree to CSR with proper loading states.
Edge cases
- •Browser extensions (Grammarly, LastPass) mutate DOM before hydration.
- •Streaming SSR boundaries can mismatch independently per chunk.
- •Hydration errors in production are harder to debug — no full diff message.
Real-world examples
- •Every Next.js app has dealt with these. Vercel's logs frequently include hydration errors from session-dependent UI. Solutions across the ecosystem: client-only wrappers, App Router cookies(), middleware that resolves cookies pre-render.