Home/Learn/Next.js/Hydration Errors and the Usual Traps

Hydration Errors and the Usual Traps

Advanced
Foundations

The server rendered one thing and the browser expected another. Hydration mismatches have a short list of causes, and knowing it turns a baffling error into a two-minute fix.

Overview

A hydration error means the HTML the server produced does not match what React builds on the client for the same props. React discards the server HTML, re-renders, and warns — so the page usually still works, which is why these get ignored until something breaks subtly. The causes are a short and finite list: values that differ between the two runs, browser-only APIs read during render, and invalid HTML nesting that the browser silently repairs. This concept is the debugging companion for everything above.

The Causes

Five, and they cover nearly every occurrence.

Time, randomness, browser APIs, bad nesting
// 1. A value that differs between server and client
new Date().toLocaleString()      // server timezone vs the user's
Math.random()
Date.now()
// Fix: render it in an effect, or pass a fixed value down.

// 2. A browser API read during render
if (window.innerWidth < 768)     // ReferenceError on the server
localStorage.getItem('theme')    // undefined on the server
// Fix: read it in useEffect, and accept one frame of the default.

// 3. Invalid HTML nesting — the browser repairs it, React does not
<p><div>…</div></p>              // the browser closes the p early
<a><a>…</a></a>
<table><div/></table>
// This one is easy to miss, because the error points at hydration
// rather than at the markup.

// 4. Extensions that mutate the DOM before hydration
//    Password managers and translators add attributes to inputs.
//    Test in an incognito window before chasing your own code.

// 5. Locale or currency formatting without a fixed locale
new Intl.NumberFormat().format(n)          // varies by machine
new Intl.NumberFormat('en-IN').format(n)   // deterministic

The Fixes

Three patterns, in order of preference.

Mount flag, suppress, or skip SSR
// A. Render it after mount — the honest default
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return <Skeleton />          // same on both sides
return <RelativeTime value={date} />

// B. suppressHydrationWarning — for a single unavoidable element
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
// It suppresses ONE level deep and is not a general escape hatch.

// C. Skip SSR for the component entirely
const Chart = dynamic(() => import('./Chart'), { ssr: false })
// Right for a library that touches window on import. It costs SEO
// and adds a loading state, so it is a last resort, not a first one.

// Theme toggles are the classic case: the server cannot know the
// user's theme, so the first paint is always a guess. The standard
// fix is a tiny blocking script in the head that sets a class on
// <html> before React runs — the page then hydrates against markup
// that already matches.

// Debug by narrowing: the error names the element. Comment out half
// the subtree, reload, repeat. It is faster than reading the diff.

Other Traps in the Same Family

Errors that are not hydration but come from the same server/client confusion.

The neighbouring error messages, decoded
// "useState only works in a Client Component"
//   -> add 'use client', or move the state down into a child

// "Functions cannot be passed directly to Client Components"
//   -> a callback prop crossing the boundary; define it in the client
//      component, or pass a Server Action instead

// "You're importing a component that needs next/headers"
//   -> cookies() or headers() used somewhere reachable from a client
//      component

// "Dynamic server usage: cookies" during build
//   -> a static route touched a dynamic API. Either accept dynamic
//      rendering, or move the cookie read into a smaller boundary.

// "Text content did not match" but only in production
//   -> almost always a date or number formatted without an explicit
//      locale, since the build machine and the user differ

// Works in dev, breaks in production
//   -> dev renders per request and skips the full route cache.
//      ALWAYS test with: npm run build && npm start
//      before concluding a caching problem is fixed.

Key Points to Remember

  • 1A hydration error means the server HTML and the client render disagree for the same props
  • 2The causes are dates, randomness, browser APIs read during render, invalid HTML nesting, and browser extensions
  • 3Prefer rendering after mount; suppressHydrationWarning covers one element and ssr: false is a last resort
  • 4Format dates and numbers with an explicit locale or the build machine and the user will disagree
  • 5Dev renders per request and skips caching — always verify with a production build before trusting a fix

Interview Questions

Sign in to ask Aria
1

What causes a hydration mismatch and why does the page often still work?

Hard
2

How would you render a relative timestamp without a hydration error?

Medium
3

Why might a caching bug appear only in a production build?

Medium

Ask Aria about Hydration Errors and the Usual Traps

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…