How To Guide

How to Fix React Hydration Mismatch Errors (Next.js Guide)

Fix the 'Hydration failed because the initial UI does not match' error in React and Next.js. The 6 top causes and proven patterns to fix each.

How to Fix React Hydration Mismatch Errors

You ship a Next.js page, open the browser, and get a wall of red text: "Hydration failed because the initial UI does not match what was rendered on the server." Every React developer meets this error eventually. It looks scary, but every single case boils down to one rule: the HTML your server sent must exactly match what the client renders on the first pass.

What SSR hydration actually is

With server-side rendering, Next.js runs your components on the server, produces an HTML string, and sends it to the browser. When React loads in the browser, it does not throw that HTML away. Instead it hydrates it: walks the existing DOM node-by-node and attaches event listeners and state, assuming the structure is identical to what it just rendered on the client. If any node differs (text content, attribute, tag, order), In React 18+, a hydration mismatch typically causes the whole hydration root to fall back to client-side rendering (not just the subtree). React logs the error and re-renders from scratch, which can be visibly disruptive. That is slow, causes visible flicker, and often breaks interactivity.

The exact error message

You will see one of these in the console:

  • Hydration failed because the initial UI does not match what was rendered on the server.
  • Text content does not match server-rendered HTML.
  • Warning: Expected server HTML to contain a matching <div> in <p>.

React usually points at the offending element. Read that pointer first before guessing.

The 6 top causes and how to fix each

1. Date.now() or Math.random() in render

Detect: The server renders id="user-4821", the client renders id="user-9134". Any value that changes every call is guaranteed to mismatch because the server generated it seconds before the client did.

Fix: Generate the value once and pass it as a prop, use React's useId() hook for stable ids, or defer generation to useEffect:

const [now, setNow] = useState(null);
useEffect(() => { setNow(Date.now()); }, []);
return <span>{now ?? '—'}</span>;

2. Accessing window, document, or localStorage during render

Detect: The server crashes with ReferenceError: window is not defined, or a theme flashes wrong on load. Anything reading window.innerWidth, localStorage.getItem, or document.cookie at render time is client-only.

Fix: Move the read into useEffect. Render a neutral default on the server (loading skeleton, empty string, or the SSR-safe fallback) and swap it after mount:

const [theme, setTheme] = useState('light');
useEffect(() => {
  setTheme(localStorage.getItem('theme') || 'light');
}, []);

3. Invalid HTML nesting

Detect: The error message says something like "cannot appear as a descendant of <p>". Common offenders: <div>, <section>, <ul>, or another <p> inside a <p>. Also <a> nested inside <a>, or table rows outside a <tbody>.

Fix: The browser silently rewrites illegal nesting when it parses the HTML, so the DOM tree no longer matches what React expects. Change the outer <p> to a <div>, or use inline elements (<span>) for the children.

4. Browser extensions modifying the markup

Detect: The mismatch only happens for some users, the diff shows attributes like bis_skin_checked, data-lastpass-icon-root, fdprocessedid, or data-grammarly-shadow-root. Grammarly, LastPass, and Bitdefender all inject attributes into rendered HTML.

Fix: You cannot control extensions. Add suppressHydrationWarning to the specific element the extension touches (usually the <body> or a form input). Do not apply it globally.

<body suppressHydrationWarning>{children}</body>

5. Locale-dependent formatting

Detect: Dates or numbers render as 7/16/2026 on the server and 16/07/2026 on the client. The server uses the OS locale (often en-US), the browser uses the user's locale.

Fix: Always pass an explicit locale to Intl.DateTimeFormat, toLocaleString, and toLocaleDateString. If you genuinely want the user's locale, render a placeholder on the server and format inside useEffect. For time-only display that is fine to differ by milliseconds, suppressHydrationWarning on the specific <time> element is acceptable.

6. Conditional rendering based on user-agent

Detect: Different markup on mobile vs desktop, or you branch on navigator.userAgent. The server has no navigator, so it cannot match.

Fix: Prefer CSS media queries for responsive rendering. When you truly need JS-based device detection, render the desktop version on the server and swap in useEffect, or use dynamic import with SSR disabled:

import dynamic from 'next/dynamic';
const MobileMenu = dynamic(() => import('./MobileMenu'), { ssr: false });

The three patterns that fix almost everything

  1. useEffect swap. Render a stable placeholder on the server; set the real value after mount. Zero mismatch, one extra render.
  2. Dynamic import with ssr: false. For components that fundamentally need the browser (rich text editors, WebGL, wallet libraries), skip SSR for that component only. The rest of the page still SSRs.
  3. suppressHydrationWarning. Use sparingly, only on the specific element with an unavoidable difference (timestamps, extension attributes). It silences the warning but React still re-renders the subtree, so it is not a performance win, just a noise reduction.

Debugging checklist

  • Open the page in an incognito window with all extensions disabled. If the error disappears, cause #4.
  • View source (Ctrl+U) and compare the raw server HTML against the hydrated DOM in DevTools.
  • Search your codebase for Date, Math.random, window., localStorage, navigator, toLocaleString.
  • Never wrap your entire app in suppressHydrationWarning to make the error go away. You are hiding a real bug, not fixing it.

Hydration errors feel random, but they never are. Every mismatch has a specific line of code producing different output on the server than the client. Find that line, apply the useEffect pattern or dynamic import, and the error is gone.

Featured Tools

Try these free tools directly in your browser — no sign-up required.

hydration mismatch react hydration error next.js hydration failed suppressHydrationWarning useEffect hydration dynamic import ssr false server client mismatch hydration failed error

Explore 300+ Free Tools

Utilko has tools for developers, writers, designers, students, and everyday users — all free, all browser-based.