•19 min read

Next.js Hydration Error: Fix React 418, 423 & Text Content Mismatch (2026)

Next.js Hydration Error: Fix React 418, 423 & Text Content Mismatch (2026)
Table of Contents(31 sections)

I still remember the first time I saw a giant red hydration error in my browser console. The message was cryptic, the stack trace pointed to a minified React file, and my UI looked completely broken for a split second before snapping into place.

I thought my database was down. It wasn't. I had just tried to render a dynamic timezone string directly in my component.

Audio Briefing
0:00 / 0:00
Interactive Dev Tool
100% Client-Side & Private

Next.js Hydration Error Matcher & Fixer

Diagnose Error #418, #423, #425 & get copy-paste code diffs

Paste Next.js or React hydration error logs to instantly diagnose server-client HTML mismatches (dates, localStorage, extension injection) and generate safe fixes.

Error 418/423SSR MismatchesSuppression FixesNext.js 14/15

Quick reference: the four causes and their fixes:

ErrorRoot causeFix
Text content mismatchwindow / localStorage in renderMove to useEffect, useState(false) init
Error 418Date/time rendered directly on serversuppressHydrationWarning or useEffect
Error 423 / 425Suspense streaming SSR boundary conflictWrap with <Suspense fallback={null}> at right boundary
Dark mode flashTheme read before hydrationInline script before React loads
<html> mismatchBrowser extension injecting attributessuppressHydrationWarning on <html>

Jump to your error:

If you're building with Next.js (or any SSR React framework), you will run into at least one of these. The sections below walk through each cause with exact copy-paste fixes.

Next.js SSR Hydration Error and Text Content Mismatch Debugging

Live Hydration Error Matcher & Code Fixer

Paste your browser console error log or JSX snippet directly below to diagnose the exact root cause and get an instant copy-paste fix:


Advertisement

What Actually Is "Hydration"?

Before we fix the error, we need to understand the process. Next.js does two things when a user requests a page:

  1. Pre-rendering (Server Side): Next.js runs your React components on the server, generates raw HTML, and sends it to the browser. The user sees the page instantly, but it isn't interactive yet (buttons don't work, inputs are inert).
  2. Hydration (Client Side): React runs on the client, downloads the JavaScript bundle, walks the server-rendered HTML structure, and attaches event listeners (like onClick) to make it alive.

Here is the golden rule of SSR: The HTML generated on the server must exactly match the HTML generated on the client's first render.

If there is even a single character of difference, React panics. It throws a hydration mismatch error, discards the server HTML, and rebuilds the page from scratch. This slows down your site and ruins the performance benefits of SSR.

PhaseWhere it RunsOutputResponsibility
Pre-rendering
Node.js Server
Raw HTML & CSS
Instant visual paint
Hydration
User Browser
Event-bound DOM
Interactive page (alive)

How to Fix "Text Content Does Not Match Server-Rendered HTML" (window & localStorage)

The most common cause of "Text content does not match server-rendered HTML" is referencing browser APIs directly in your component render path. During server-side pre-rendering, Node.js has no concept of window, document, or localStorage.

// ❌ THIS WILL CRASH ON THE SERVER OR MISMATCH
function MyComponent() {
  const width = window.innerWidth; // window is undefined on the server!
  return <div>Width: {width}</div>;
}

Even if you wrap it in a typeof window !== 'undefined' check to prevent a hard crash, you will still trigger a hydration error because the server output (<div>Desktop</div>) differs from what the browser renders on initial paint.

tsx
1-// ❌ Hydration Error: server gets "Server", client gets width
1+// ✅ Safe: state is initialized on client mount
2+import { useState, useEffect } from 'react';
3+
24 function ResponsiveLayout() {
3- const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
5+ const [isMobile, setIsMobile] = useState(false);
6+
7+ useEffect(() => {
8+ setIsMobile(window.innerWidth < 768);
9+ }, []);
10+
411 return <div>{isMobile ? 'Mobile' : 'Desktop'}</div>;
512 }
The Hook Strategy

For browser APIs, always defer the state update to useEffect. Since useEffect only runs in the browser after mount, it never executes on the server. The initial HTML matches, and the client updates the UI safely afterward.


How to Fix React Error #418 & Dynamic Content (Dates, Timestamps & Math.random)

Minified React Error #418 occurs when server-rendered HTML text differs from client-rendered text. If you render the current timestamp or a random number in JSX, the server will output one string (e.g. at build or request time), while the client browser outputs a different string milliseconds later.

// ❌ Server: "Rendered at 10:00:00", Client: "Rendered at 10:00:02" -> Error #418
function CurrentTime() {
  return <div>Time: {new Date().toLocaleTimeString()}</div>;
}

There are two ways to solve this depending on what you need.

If the data doesn't need to be there for SEO, wait until the component mounts to show it.

Initialize Mount State

Create a boolean state to track if the component has mounted in the browser.

Trigger in useEffect

Flip the boolean to true inside useEffect.

Conditional Render

Show a loading placeholder on the server and the actual date on the client.

import { useState, useEffect } from 'react';

export default function CurrentTime() {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return <div>Loading time...</div>;

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

Solution B: Suppress Warning (For Static Content)

If the discrepancy is harmless (like a static timezone rendering or an intentional mismatch), you can tell React to ignore it using the suppressHydrationWarning attribute.

// This disables the console error for this specific element
<div suppressHydrationWarning>Time: {new Date().toLocaleTimeString()}</div>
Warning

suppressHydrationWarning only works one level deep and does not fix the underlying performance cost of React mismatch rebuilding. Use it sparingly for simple text nodes (like dates or timestamps), not large UI trees.


Advertisement

Fixing Invalid HTML Tag Nesting Mismatches (<div> Inside <p>)

Invalid HTML tag nesting is the sneakiest cause of hydration errors. It has nothing to do with JavaScript state or timing, but rather how browser DOM parsers automatically repair invalid HTML syntax.

If you nest a <div> inside a <p> tag, the browser's HTML parser will automatically close the <p> tag before the <div> is opened. This transforms your intended DOM structure into something completely different.

React expects the DOM structure parsed by the browser to match the virtual DOM tree generated on the server. When the browser forcibly rewrites and closes tags, React spots the mismatched tree depth and throws: "In HTML, <div> cannot be a descendant of <p>".

html
1-// ❌ Invalid HTML: browser forces <p> to close before <div>
1+// ✅ Valid HTML: use divs or spans instead
2-<p>
2+<div>
33 Check this out:
44 <div>Nested container</div>
5-</p>
5+</div>

Common HTML nesting mistakes that trigger hydration errors:

  • Placing <div> or <p> inside another <p>
  • Placing <ul> or <ol> directly inside a <p>
  • Putting a <tr> directly under a <table> instead of wrapping it in <tbody>
  • Putting block elements (h1, form, section) inside inline elements (a, span)

How to Debug Hydration Errors: Step-by-Step

Next.js 14+ gives you much better error overlays than before: but the trace can still be cryptic. Here is the exact workflow I use every time:

Step 1: Read the error diff in the overlay

When a hydration error fires in development, Next.js shows a red overlay with the server output and client output side by side. The first thing to do is read both sides carefully:

Server rendered:
  <div class="theme-dark">

Client rendered:
  <div class="theme-light">

This immediately tells you the mismatch is theme-related: look for localStorage or cookies being read during render.

Step 2: Test in Incognito with extensions disabled

Browser extensions are a silent hydration killer. Ad blockers, grammar checkers, and translator extensions inject DOM nodes or modify attributes before React hydrates. The server never saw those changes, so React panics.

Always reproduce the error in an Incognito window before you do anything else. If the error disappears in Incognito, a browser extension is your culprit: not your code.

Step 3: Enable Next.js Turbopack for better diffs

next dev --turbopack

Turbopack gives more precise hydration error messages than the default Webpack mode, including the exact component path and a character-level diff of the mismatch.

Step 4: Bisect with suppressHydrationWarning temporarily

If you can't find the source, add suppressHydrationWarning to your root <body> temporarily. This silences all hydration errors. Then remove it from one child component at a time until the error reappears: that's your culprit component.

// Temporary! Remove after finding the bug.
<body suppressHydrationWarning>
Remove After Debugging

suppressHydrationWarning on <body> silences all hydration warnings globally. It's a debugging tool only: never ship it to production as a permanent fix.

Step 5: Search for the common patterns with grep

Run these in your project root to find the most likely offenders:

# Find direct localStorage / sessionStorage reads in render paths
grep -rn "localStorage\|sessionStorage" src/ --include="*.tsx" --include="*.ts" | grep -v "useEffect"

# Find components using window without a guard
grep -rn "window\." src/ --include="*.tsx" | grep -v "typeof window"

# Find suppressHydrationWarning usage (should be rare)
grep -rn "suppressHydrationWarning" src/ --include="*.tsx"

# Find dynamic imports without ssr:false on heavy chart/map components
grep -rn "import dynamic" src/ --include="*.tsx"

Each result from the first two greps is a potential hydration error waiting to happen.


Fixing Browser Extension & Third-Party DOM Script Hydration Errors

This is the hardest category to debug because the bug isn't in your code at all. Libraries like jQuery plugins, legacy analytics scripts, or embedded widgets sometimes manipulate the DOM directly: adding attributes, wrapping nodes, or injecting elements.

React's hydration expects the DOM to be exactly what the server produced. Any external DOM mutation before hydration completes will cause a mismatch.

Common offenders:

  • Google Translate widget: rewrites all text nodes in the DOM
  • Hotjar / FullStory: inject iframe or attribute overlays
  • Legacy jQuery plugins: call .addClass() or .wrap() on mount
  • Browser translate feature: same as Google Translate widget

The fix: Load these scripts after hydration using next/script with strategy="afterInteractive":

import Script from 'next/script'

export default function Layout({ children }) {
  return (
    <>
      {children}
      {/* Load third-party DOM scripts only after React has hydrated */}
      <Script
        src="https://example.com/legacy-widget.js"
        strategy="afterInteractive"
      />
    </>
  )
}

For Google Translate specifically, the only reliable fix is to disable the browser's built-in Translate prompt using a meta tag: Google Translate rewrites text nodes in a way that is fundamentally incompatible with React's hydration:

<!-- In your <head>: disables browser translate prompt -->
<meta name="google" content="notranslate" />

Summary Checklist

MistakeWhy it FailsThe Safe Pattern
window / localstorage
Fails: undefined on server
Use state initialized in useEffect
new Date() / Math.random()
Fails: values differ between runs
Defer rendering via mounted state
<div> inside <p>
Fails: browser rewrites invalid HTML
Keep tag structures semantic and valid
Third-party DOM scripts
Fails: mutate DOM before hydration
Use strategy="afterInteractive"

Resolving React Error #423 in Suspense Boundaries & Streaming SSR

React 18 introduced streaming server-side rendering via Suspense. While this dramatically improves Time to First Byte, it introduces Minified React Error #423 and #425 when hydration boundaries conflict during streaming chunks.

When you wrap content in <Suspense>, React streams a fallback (like a spinner) to the browser first, then streams the resolved content when it's ready. The hydration mismatch happens when the client tries to reconcile a component that was streamed mid-render with a different local state.

// ❌ This can cause subtle hydration mismatches with streaming SSR
export default function Page() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserDashboard /> {/* Fetches data async */}
    </Suspense>
  );
}

// Inside UserDashboard: DON'T do this
function UserDashboard() {
  const [theme] = useState(localStorage.getItem('theme')); // ❌ localStorage in SSR!
  // ...
}

The safe pattern with Suspense + streaming:

// ✅ Correct: keep client-only state inside useEffect, not initial state
function UserDashboard() {
  const [theme, setTheme] = useState<string | null>(null); // null = server-safe default

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

  return <div data-theme={theme ?? 'light'}>...</div>;
}
React 18 Streaming Rule

Any component inside a Suspense boundary that reads browser APIs must use the useEffect + mounted state pattern. The fallback render runs on the server, and the resolved render must match the initial client render exactly before useEffect fires.


Fixing 'use client' Serialization Boundary Mismatches in App Router

The App Router's Server Component model introduces a new category of hydration errors that didn't exist in the Pages Router. The core rule:

Server Components cannot pass non-serializable props to Client Components.

If you pass a function, a class instance, a Date object, or undefined through a Server → Client component boundary, Next.js will throw an error at build time or cause a silent hydration mismatch at runtime.

// ❌ This silently breaks hydration: Date is not serializable over the wire
// app/page.tsx (Server Component)
import ClientWidget from './ClientWidget'; // has 'use client'

export default function Page() {
  const timestamp = new Date(); // Date object: NOT serializable!
  return <ClientWidget createdAt={timestamp} />;
}
// ✅ Serialize to ISO string first: strings are always safe to pass
export default function Page() {
  const timestamp = new Date().toISOString(); // plain string ✅
  return <ClientWidget createdAt={timestamp} />;
}

// In ClientWidget: reconstruct on the client
'use client'
export function ClientWidget({ createdAt }: { createdAt: string }) {
  const date = new Date(createdAt); // safe to construct on client
  return <div>{date.toLocaleDateString()}</div>;
}

Common non-serializable types that break the boundary:

TypeErrorFix
DateSilent hydration mismatchPass .toISOString() string
FunctionBuild errorMove function to Client Component
undefinedProp becomes nullUse explicit null default
Class instanceSilent mismatchPass plain object {} instead
BigIntSerialization errorConvert to String(bigint)

For a deep dive into separating Server and Client boundaries in large Next.js apps, see the Next.js App Router folder structure guide and the Next.js App Router caching strategies guide.


How to Test for Hydration Errors Before They Hit Production

The best hydration error is one you catch locally, not in a 3am Sentry alert. Here's a testing strategy that catches all four villain patterns before deploy.

1. Enable React Strict Mode (it's already on in Next.js)

Next.js enables <React.StrictMode> by default. In development, this intentionally renders components twice to surface side effects and hydration issues. If you see a component flash or double-render in dev: that's Strict Mode working. Don't disable it.

2. Write a Mounted Guard Unit Test

Use React Testing Library with user-event v14 to assert that components render the server-safe default before hydration:

import { render, screen } from '@testing-library/react'
import CurrentTime from './CurrentTime'

test('renders server-safe placeholder before mount', () => {
  render(<CurrentTime />)
  // On first render (simulating SSR), should show placeholder
  expect(screen.getByText('Loading time...')).toBeInTheDocument()
})

3. Check for suppressHydrationWarning Overuse

Run this grep before every PR to find suppressed warnings you may have forgotten:

grep -r "suppressHydrationWarning" src/ --include="*.tsx"

Each hit should have a comment explaining why. If there's no comment, it's a hidden bug.

4. Use Next.js --turbopack dev mode

Turbopack (Next.js 14+) gives more precise hydration error messages than Webpack, including the exact diff between server and client HTML. Enable it:

next dev --turbopack

Next Steps

Now that you know how to dodge hydration errors, audit your component tree architecture. For large applications, following a clean Next.js App Router folder structure helps keep Server and Client component boundaries strictly separated. If you are building data-driven interactive forms, check out our guide on React 19 Server Actions and Optimistic Updates to handle mutations without hydration flash, or learn how to wire up direct database queries using Next.js App Router with Prisma.

Frequently Asked Questions

What causes React error 418 in Next.js?

React error 418 means the server-rendered HTML doesn't match what React expected to find during hydration. Common causes are rendering window, localStorage, new Date(), or user-specific data directly in JSX during the initial render.

Does a browser extension cause hydration errors in Next.js?

Yes. Browser extensions like Grammarly, 1Password, and dark-mode extensions inject attributes (like data-new-gr-c-s-check-loaded or data-beasties-container) into <html> or <body> after server rendering. Add suppressHydrationWarning to the <html> element in your root layout to silence these.

How do I fix text content mismatch in Next.js?

Move any client-specific value (locale date strings, user names, window dimensions) into a useEffect that sets state after mount. Use useState(null) as the initial value and render a fallback or null until mounted.

What does suppressHydrationWarning do?

suppressHydrationWarning tells React to skip the hydration mismatch warning for that specific element. Use it on <html> and <body> tags to avoid false-positive warnings from browser extensions, but do NOT use it to silence real application bugs.


Paste your error logs or component code into our free Next.js Hydration Error Matcher & Fixer Tool for real-time root-cause analysis and copy-paste code solutions.


Frequently Asked Questions

What is a hydration error in Next.js?

A hydration error occurs when the HTML the server sends to the browser doesn't match what React tries to render on the client. React compares its virtual DOM tree against the server-rendered HTML during "hydration" - the process of attaching event handlers and making the page interactive. Any mismatch causes React to throw an error (commonly Error #418) and re-render from scratch, which can cause a visible flash of unstyled content.

What does suppressHydrationWarning do and when should I use it?

suppressHydrationWarning={true} tells React to silently accept a mismatch on a specific element without throwing an error or re-rendering. Use it only for elements whose content is intentionally different between server and client - like a timestamp, a random ID, or a browser-extension-injected attribute. Never use it to silence bugs you haven't diagnosed. Applied to <html> it silences browser extension attribute injection. Applied to a <time> element it silences dynamic date rendering.

Why does my Next.js hydration error say args[]=html?

The args[]=html pattern in the console error (often React Error 418) means a browser extension is injecting attributes into your <html> or <body> tag before React hydrates. Common culprits: Grammarly, LastPass, Dark Reader, and browser translate features. The fix is suppressHydrationWarning on your root <html> element in app/layout.tsx. This error is not in your code.

What is the difference between React Error 418, 423, and 425?

  • Error 418: Hydration mismatch - server HTML doesn't match client render. Most common.
  • Error 423: The component was suspended during hydration (Suspense boundary hit too early during streaming SSR). Fix by placing <Suspense> boundaries above components that read async data.
  • Error 425: "Text content does not match" specifically during selective hydration. Usually caused by date/time values or Math.random() rendering different values on server vs client. Fix with useEffect deferral.

How do I fix "Text content does not match server-rendered HTML" in Next.js?

This error means you're rendering something that produces different output on the server vs the client. Common causes: new Date(), Math.random(), window.innerWidth, or localStorage. Fix: initialize with a stable value (null, false, or an empty string) and update in useEffect after mount. Never access browser APIs during the initial render phase.

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement