Back to Next.js
Next.js
medium
mid

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.

8 min read·~5 min to think through

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.

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

tsx
<p>{new Date().toLocaleDateString()}</p>
// Server: 'en-US' (Vercel default)
// Client: 'fr-FR' → mismatch

3. typeof window !== 'undefined' branches.

tsx
return <div>{typeof window === 'undefined' ? 'server' : 'client'}</div>;

Server renders 'server', client renders 'client'.

4. Browser-only APIs.

tsx
const isMobile = window.innerWidth < 768; // crashes on server, or different value

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

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

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

tsx
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:

tsx
<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:

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

tsx
<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 suppressHydrationWarning to inputs (it suppresses just that element).
  • Detect via useEffect and re-render.

Reading the error

Next.js prints both trees. Find the first difference:

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

Senior engineer discussion

Senior framing: hydration is fragile by design. Server and client must produce identical output from identical inputs. The fix isn't 'add more checks' — it's 'reduce inputs that diverge'. Push variability into useEffect or resolve it on the server before render.

Related questions