Fix Next.js Hydration Mismatch Errors (2026 Complete Guide)

The error:

A tree hydrated but some attributes of the server rendered HTML didn't
match the client properties. This won't be patched up. This can happen
if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes
  each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with
  the HTML.
- Invalid HTML tag nesting.

TL;DR: the HTML your server rendered is different from what React rendered on the client. The most common 2026 causes are locale-aware date/number formatting, third-party scripts that mutate the DOM, and browser-specific globals (window, navigator, localStorage) called during render.

What causes Next.js hydration mismatch?

| Cause | How it shows up | Fix | |---|---|---| | Date formatting differs by locale | new Date().toLocaleString() returns one string on server, another on client | Pass locale explicitly, or render only after useEffect | | Math.random() / Date.now() in render | Different value each render | Move to a useState initialized in useEffect | | window, navigator, localStorage access during render | Server has no window, throws or undefined | Use useEffect, or typeof window !== "undefined" guard, or a useIsClient hook | | Browser extensions injecting HTML | <body> gains data-extension-* attributes after page load | Add suppressHydrationWarning to <body> | | Third-party scripts mutating DOM | Analytics, chat widgets, A/B testing | Load via next/script with strategy="afterInteractive" | | Invalid HTML nesting (<div> inside <p>) | The browser silently rewrites the tree | Fix the markup | | next-intl or react-i18next flashing | First paint shows source language, then swaps | See the next-intl flash fix or use a zero-flash i18n library | | Server / client time zone differences | new Date().toLocaleString("en", { timeZone: "Europe/Paris" }) on server, browser default on client | Set explicit timeZone everywhere |

Step-by-step debugging

Step 1 — Read the React DevTools warning carefully

The browser console shows the exact element where the mismatch occurred. Expand the stack trace; React 18 highlights the component and prop that differed.

Step 2 — Disable browser extensions

Open the page in a private/incognito window with all extensions disabled. If the error disappears, an extension is mutating your HTML. Add suppressHydrationWarning to your <html> and <body> tags:

// app/layout.tsx
<html lang="en" suppressHydrationWarning>
  <body suppressHydrationWarning>
    {children}
  </body>
</html>

This is a safe targeted suppression — it only stops the warning for the attributes of these elements, not for their children.

Step 3 — Audit for browser globals during render

Search your codebase for these patterns:

grep -rn "typeof window" src/
grep -rn "localStorage" src/
grep -rn "Math.random()" src/
grep -rn "Date.now()" src/
grep -rn "new Date()" src/ | grep -v ".test."

For each hit, ask: does this run during render? If yes, move it to useEffect.

Step 4 — Use a useIsClient hook

import { useEffect, useState } from "react";
 
export function useIsClient() {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => setIsClient(true), []);
  return isClient;
}
 
// Usage
function MyComponent() {
  const isClient = useIsClient();
  if (!isClient) return null; // or a server-safe placeholder
  return <div>{new Date().toLocaleString()}</div>;
}

Step 5 — Identify i18n flash specifically

If your error message includes attribute differences on text content (children), and you use next-intl, react-i18next, or any DOM-injection translation tool (e.g. Weglot), you have an i18n hydration mismatch. The page rendered in the source language on the server, then the i18n library swapped to the user's locale on the client.

Fixes:

  1. Set the locale before render. With next-intl, pass locale from the URL segment or middleware header to the NextIntlClientProvider. Server must know the locale at render time, not detect it client-side.
  2. Use a zero-flash React i18n library. Lovalingo renders the correct locale before paint by integrating into the React tree at the provider level. No client-side language swap.
  3. Use locale-prefixed URLs. /en/page, /fr/page — never a single URL that detects locale at runtime.

Next.js-specific i18n hydration fix

If you use the Next.js App Router with next-intl:

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
 
export default async function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  const messages = await getMessages();
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

The critical detail: <html lang={locale}> is set from the server-known locale, not from navigator.language (which only exists on the client).

If you use Lovalingo instead:

// app/layout.tsx
import { LovalingoProvider } from "@lovalingo/lovalingo";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <LovalingoProvider
          publicAnonKey={process.env.NEXT_PUBLIC_LOVALINGO_KEY!}
          defaultLocale="en"
          locales={["fr", "es", "de", "it", "pt", "zh", "ar", "ja"]}
          routing="path"
        >
          {children}
        </LovalingoProvider>
      </body>
    </html>
  );
}

Lovalingo handles the locale resolution server-side via the URL path, so there is no client-side flash to recover from.

When suppressHydrationWarning is OK

suppressHydrationWarning is appropriate when:

  • A browser extension is the cause (you cannot control it)
  • Date / time / locale formatting is intentionally per-user
  • A third-party script mutates a specific element you control

It is NOT appropriate when:

  • You have a real bug that will cause inconsistent UX
  • You are using it on <body> or a large subtree just to silence the warning

Updated 2026-05-22 by Charles Perret.