Fixing Next.js 'Text content does not match server-rendered HTML' Hydration Error
Development3 min read

Fixing Next.js 'Text content does not match server-rendered HTML' Hydration Error

You load your Next.js application. The page renders instantly (thanks, SSR!). But then, a split second later, the console explodes with red text: Error: Text content does not match server-rendered HTML. Warning: Expected server HTML to contain a matching <div> in <div>.

This is a Hydration Error. It means the basic HTML template the server sent did not match exactly what React tried to build when it "woke up" (hydrated) in the browser.

Root Cause Analysis

React hydration expects a pixel-perfect match between the Server initial state and the Browser initial state. Common causes of mismatches:

  1. Time/Randomness: Rendering new Date().toLocaleTimeString() or Math.random(). The server time is different from the browser time.
  2. Window Object: Checking if (typeof window !== 'undefined') to render a component during the initial return.
  3. Invalid HTML: Nesting a <p> inside another <p> or a <div> inside a <p>. The browser auto-corrects this HTML before React sees it, causing a mismatch.

The Solution: Step-by-Step

1. The useEffect Fix (For Time/Randomness)

If you need to show the current time, local storage data, or anything browser-specific, you must not render it on the first pass.

Bad Code:

// Fails because server render != client render
return <div>{new Date().toLocaleTimeString()}</div>

Good Code:

const [mounted, setMounted] = useState(false);
useEffect(() => {
    setMounted(true);
}, []);

if (!mounted) return null; // Or a loading skeleton

return <div>{new Date().toLocaleTimeString()}</div>

By waiting for useEffect, you force the update to happen after hydration is complete.

2. The suppressHydrationWarning Prop (The escape hatch)

Sometimes you just need to render a timestamp and you don't care if it flickers. React provides specific prop to ignore the warning on a specific element.

<div suppressHydrationWarning>
  {new Date().toISOString()}
</div>

Note: This only works one level deep. Use sparingly.

3. Check for Invalid HTML Nesting

This is the silent killer. React code:

<p>
  <div>I am a div inside a paragraph</div>
</p>

Technically, HTML5 forbids block elements (div) inside formatting elements (p). The browser parses the HTML from the server, closes the p tag early, and leaves the div orphaned. When React tries to attach to the div, it can't find it inside the p.

Fix: Change the parent <p> to a <div> or a <span>.

Alternative Fix: Dynamic Imports

If a specific component (like a map or a rich text editor) is causing the issue because it relies heavily on the window object, import it dynamically with SSR disabled.

import dynamic from 'next/dynamic'

const NoSSRComponent = dynamic(() => import('../components/Map'), {
  ssr: false,
})

This tells Next.js: "Don't even try to render this on the server. Just send a placeholder."

Expert Tip: The Chrome Extension Gotcha

[!TIP] Disable Your Browser Extensions I once debugged a hydration error for 3 hours. The culprit? LastPass and Grammarly. These extensions inject extra HTML attributes or DOM nodes into your page before React hydrates. Typically React v18 is good at ignoring these, but sometimes they conflict. Test in Incognito Mode. If the error disappears, it's not your code; it's your browser extensions modifying the DOM.

Conclusion

Hydration errors are strict for a reason—they prevent UI corruption. By ensuring your initial render is deterministic (identical on server and client), you ensure a snappy, flicker-free experience for your users.