๐ŸŒ Detecting your locationโ€ฆ

How to Fix Hydration Failed Error in Next.js: Complete Solution

โฑ๏ธ5 min read  ยท  967 words

The error Hydration failed because the initial UI does not match what was rendered on the server (or “Text content does not match server-rendered HTML”) in Next.js means the HTML the server rendered differs from what React renders on the client. Here’s why it happens and how to fix every cause.

What Hydration Is

Next.js renders your page to HTML on the server (SSR), sends it to the browser for fast initial display, then React “hydrates” it โ€” attaching interactivity. Hydration requires the server-rendered HTML and the client’s first render to match exactly. If they differ, React throws a hydration error because it can’t reconcile the mismatch.

Cause 1: Using Browser-Only APIs During Render

// ๐Ÿ› window/localStorage don't exist on the server โ†’ mismatch
function Component() {
  const theme = localStorage.getItem('theme');   // โŒ undefined on server
  return <div className={theme}>...</div>;
}

// โœ… Access browser APIs only after mount (in useEffect)
function Component() {
  const [theme, setTheme] = useState(null);

  useEffect(() => {
    setTheme(localStorage.getItem('theme'));   // runs only on client
  }, []);

  return <div className={theme || 'default'}>...</div>;
}

Cause 2: Dates and Times

// ๐Ÿ› The server and client render at different times โ†’ mismatch
function Component() {
  return <div>{new Date().toLocaleString()}</div>;   // โŒ differs
}

// โœ… Render the date only on the client
function Component() {
  const [date, setDate] = useState(null);
  useEffect(() => { setDate(new Date().toLocaleString()); }, []);
  return <div>{date ?? 'Loading...'}</div>;
}

Cause 3: Random Values

// ๐Ÿ› Math.random() produces different values on server vs client
function Component() {
  const id = Math.random();   // โŒ different each render
  return <div id={id}>...</div>;
}

// โœ… Use React's useId for stable IDs, or generate in useEffect
import { useId } from 'react';
function Component() {
  const id = useId();   // stable across server and client
  return <div id={id}>...</div>;
}

Cause 4: Invalid HTML Nesting

// ๐Ÿ› Invalid nesting gets "corrected" by the browser, causing mismatch
<p>
  <div>Content</div>   {/* โŒ div inside p is invalid */}
</p>
// The browser moves the div out, but React's tree still has it nested

// โœ… Use valid HTML nesting
<div>
  <div>Content</div>   {/* valid */}
</div>
// Common culprits: div/p inside p, block elements inside inline elements,
// invalid table structure

Cause 5: Browser Extensions Modifying HTML

// Some browser extensions inject attributes/elements into your HTML
// before React hydrates, causing a mismatch (e.g., Grammarly, dark mode extensions)

// This often shows as a mismatch on the body or specific elements.
// You can suppress the warning on a specific element if needed:
<body suppressHydrationWarning>
  {/* Use sparingly - only when the mismatch is expected/harmless */}
</body>
// But first verify it's an extension, not a real bug in your code.

Cause 6: Conditional Rendering Based on Client State

// ๐Ÿ› Rendering differently based on something only known on the client
function Component() {
  const isMobile = window.innerWidth < 768;   // โŒ window undefined on server
  return isMobile ? <Mobile /> : <Desktop />;
}

// โœ… Start with a consistent server render, adjust after mount
function Component() {
  const [isMobile, setIsMobile] = useState(false);   // consistent default

  useEffect(() => {
    setIsMobile(window.innerWidth < 768);
    const onResize = () => setIsMobile(window.innerWidth < 768);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  return isMobile ? <Mobile /> : <Desktop />;
}

The Client-Only Component Pattern

// For components that genuinely can't be server-rendered,
// use dynamic import with ssr: false
import dynamic from 'next/dynamic';

const ClientOnlyChart = dynamic(() => import('./Chart'), {
  ssr: false,   // skip server rendering entirely
  loading: () => <div>Loading chart...</div>,
});
// The component renders only on the client - no hydration mismatch possible

Frequently Asked Questions

Q: What causes hydration errors most often?
A: Using browser-only APIs (window, localStorage) during render, rendering dates/times or random values that differ between server and client, and invalid HTML nesting. The common thread: something renders differently on the server than the client. Move client-specific logic into useEffect.

Q: How do I use localStorage without hydration errors?
A: Don’t access it during render (it’s undefined on the server). Read it in useEffect (which runs only on the client) and store the value in state. Render a consistent default initially, then update after mount. This keeps server and client renders matching.

Q: My hydration error is caused by a browser extension. What do I do?
A: Extensions like Grammarly inject attributes before hydration. Verify it’s the extension (test in incognito with extensions disabled). If it’s harmless, you can add suppressHydrationWarning to the affected element โ€” but use it sparingly and only for confirmed harmless external modifications, not to hide real bugs.

Q: When should I use dynamic import with ssr: false?
A: For components that genuinely can’t or shouldn’t be server-rendered โ€” those relying heavily on browser APIs, third-party client-only libraries, or that don’t benefit from SSR. It skips server rendering entirely, eliminating hydration mismatches for that component, at the cost of no SSR benefit for it.

Q: Why does it work in development but the error appears sometimes?
A: Hydration mismatches can be intermittent when they depend on timing (dates), randomness, or external factors (extensions). They’re real bugs even when intermittent. Reproduce reliably by identifying what differs between server and client render โ€” usually browser APIs, time, or random values.

Conclusion

Next.js hydration errors mean the server-rendered HTML doesn’t match the client’s first render. The causes are consistent: browser-only APIs (window, localStorage) used during render, dates/times or random values that differ, invalid HTML nesting, and client-specific conditional rendering. The fix pattern is the same: move client-specific logic into useEffect (which runs only on the client), render a consistent default initially, and update after mount. Use useId for stable IDs, valid HTML nesting, and dynamic(..., ssr: false) for genuinely client-only components. Once your server and client renders produce matching HTML, hydration succeeds. The key mental model: whatever renders must be identical on server and client for the initial render โ€” anything client-specific belongs in useEffect.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work โ€” which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *

๐ŸŒ Read in:๐Ÿ‡ฌ๐Ÿ‡ง English๐Ÿ‡ฉ๐Ÿ‡ช Deutsch๐Ÿ‡ง๐Ÿ‡ท Portuguรชs๐Ÿ‡ธ๐Ÿ‡ฆ ุงู„ุนุฑุจูŠุฉ๐Ÿ‡ฎ๐Ÿ‡ณ เคนเคฟเคจเฅเคฆเฅ€๐Ÿ‡ง๐Ÿ‡ฉ เฆฌเฆพเฆ‚เฆฒเฆพ