Fix Hydration Failed Errors in Next.js

Quick answer

Fix a Next.js hydration error by making the server HTML and the first browser render produce the same tree, text, attributes, and IDs. Move reads of localStorage, window, media queries, local time, and random values out of render; pass stable server data; repair invalid HTML; then rule out extensions, CDN transforms, and CSS-in-JS configuration.

The Next.js error usually appears as one of these messages:

Hydration failed because the initial UI does not match what was rendered on the server.

Warning: Text content did not match. Server: "..." Client: "..."

React is not complaining that an effect changed the page after load. It is reporting a more specific contract failure: the HTML from the server differs from the output React produces for its first render in the browser.

That distinction matters. A fast workaround often makes the warning disappear while leaving an incorrect DOM, a flash of the wrong state, or an entire client-rendered subtree in production. The reliable fix is to find the non-deterministic input and give both sides the same initial result.

Quick Answer

Fix a Next.js hydration error by making the server HTML and the first browser render produce the same tree, text, attributes, and IDs. Move reads of localStorage, window, media queries, local time, and random values out of render; pass stable server data; repair invalid HTML; then rule out extensions, CDN transforms, and CSS-in-JS configuration.

TL;DR

  • Hydration attaches React behavior to server HTML. It expects an identical first client render.
  • "use client" does not mean browser-only initial render. Client Components can still be rendered into the initial server response.
  • Use useEffect for browser-only preferences and APIs. Render a stable fallback first.
  • Pass an ISO timestamp, request snapshot, feature flag, or session-derived value from the server rather than calculating a different one on each side.
  • Use useId for rendered accessibility IDs. Never use Math.random() or an incrementing module variable for them.
  • Invalid HTML can be repaired by the browser before React sees it. Compare View Source with the Elements panel.
  • Treat suppressHydrationWarning as a narrow leaf-level escape hatch, not a root-layout fix.

Symptoms

What you seeWhat it usually meansFirst place to look
Hydration failed because the initial UI does not matchThe DOM shape, element type, or rendered branch differsConditional rendering and invalid HTML nesting
Text content did not matchBoth sides rendered a text node with different valuesDate, locale, user data, random value, or cache
Attribute mismatch such as className or data-*Server and client computed different attributesTheme, CSS-in-JS, feature flags, extension-added attributes
Error only on a real phoneThe browser or device changes markup or environment dataiOS linkification, locale, timezone, extension-like injected tools
Error only after deploymentProduction input or response mutation differs from local developmentCDN, cookies, edge middleware, build configuration, CSS extraction
Page becomes interactive late or loses stateReact recovered by replacing part of the treeFix the first mismatch; do not ignore the warning

Common Causes

CauseWhy server and client differDurable repair
new Date(), Date.now(), relative timeDifferent clock, timezone, locale, or render momentPass a stable ISO value; format locally after hydration if needed
Math.random() or crypto.randomUUID() in renderA new value is created in each environmentPass a stable ID from data or use useId for accessibility wiring
localStorage, sessionStorage, windowThe server has no browser state; a guarded branch can still differRender a shared default, then read in useEffect
matchMedia, viewport width, navigatorBrowser device state is unavailable or different on the serverPrefer CSS for layout; defer behavior to an effect
Invalid HTML nestingThe browser parser rewrites the server HTML into a different DOM treeUse valid, semantic nesting and inspect parsed DOM
Cookies, auth state, A/B flags, live dataThe browser uses a different cache or stale source than the requestMake the server request snapshot the initial client prop
CSS-in-JS class generationHash or insertion order differs between server and clientFollow the library's Next.js SSR integration exactly
Extension, translation, CDN, edge transformSomething changes the DOM or response before hydrationReproduce cleanly; disable HTML rewriting and isolate third-party scripts

Root Cause: Hydration Compares Two Initial Snapshots

Server-side rendering gives the visitor visible HTML before the JavaScript bundle arrives. Hydration is the browser step where React takes ownership of that existing markup and attaches event handlers and state to it.

React expects the component tree passed to hydration to produce the same output as the tree that produced the server response. It does not exhaustively repair every difference because doing that validation and repair on every page would be expensive. A mismatch can range from a warning to a recoverable client re-render; in the worst case, logic can attach to the wrong element. React's hydrateRoot reference is clear: treat mismatches as bugs.

The hydration contract between server HTML and the first client renderThe server produces an HTML snapshot. The browser parses it, then React renders the same component tree for the first client pass. When the tree and data are deterministic, React attaches event handlers. Dates, browser storage, invalid HTML, or external DOM mutations can make the two snapshots differ and cause a hydration mismatch.One deterministic first render is the contractServer renderroute data + cookiesHTML snapshotresponseBrowser parsercreates the DOMbefore React runsJavaScript loadsFirst client rendersame props + same treeexpected outputHydrateattacheventsInputs that break equality before hydrationNon-determinismDate, locale, randomdifferent valuesBrowser-only statestorage, media querywindow during renderChanged DOM shapeinvalid nestingbrowser repairs HTMLExternal mutationextension, CDN, CSSdifferent DOM or classMismatch: warning, recovery, or client re-render
The hydration contract between server HTML and the first client render

The rule: deterministic until hydration completes

An initial render may depend on:

  • route parameters and data fetched for that request;
  • cookies or headers deliberately read on the server;
  • props serialized from Server Components to Client Components; and
  • constants committed with the build.

It must not depend on values that are different by location, moment, browser profile, or DOM mutation. The page can absolutely change after hydration. The important boundary is that it changes after both environments have agreed on the starting snapshot.

Why "use client" does not prevent this

In the Next.js App Router, a Client Component can use state, effects, and event handlers. Next.js still renders its initial HTML as part of the server response. Therefore, this component has a hydration risk:

"use client"
 
export function AccountGreeting() {
  const name =
    typeof window === "undefined"
      ? "Guest"
      : window.localStorage.getItem("name") || "Guest"
 
  return <p>Welcome, {name}</p>
}

The server emits Welcome, Guest. A browser that has "Asha" in storage renders Welcome, Asha before hydration. The typeof window guard prevents a server crash; it does not make both render passes equal.

This behavior applies to the Pages Router too. The APIs differ, but the server-rendered response and first browser render must still agree.

Step-by-Step Solution

A practical decision tree for Next.js hydration mismatch errorsStart by checking whether the server HTML was structurally changed by the HTML parser or an extension. Then look for browser-only reads during render, time or random values, and deployment-layer mutations. Each branch points to a specific repair rather than silencing the warning.Hydration mismatch reproducedDoes View Source differfrom the parsed Elements DOM?yesFix DOM mutationvalid HTML, disableextension / CDNnoDoes render read browserstate or environment data?yesRender stable fallbackthen read it inuseEffectnoDoes output use time,locale, random, or IDs?yesPass a server valueuse useId, or deferlocal formattingnoCompare request data, CSS-in-JS setup, and third-party codeDo not start with suppressHydrationWarning.It hides one leaf-level warning, not the cause.
A practical decision tree for Next.js hydration mismatch errors

Step 1: Reproduce the mismatch in a production-shaped run

Development overlays are helpful, but start by confirming the deployed behavior. Build the same app configuration used in CI, serve it, and use a private browser window with extensions disabled:

npm run build
npm run start

Open the affected route and preserve the browser console. Then compare:

  1. View Source: the raw HTML response sent by Next.js or your CDN.
  2. Elements: the DOM after the browser parser and any early scripts or extensions have touched it.
  3. The React component stack in the error: the first named component is usually closer to the cause than the page root.

If View Source already contains the wrong value, debug server data or server rendering. If View Source is correct but Elements differs before React runs, look for invalid markup, an extension, browser linkification, injected scripts, or a response transform.

Step 2: Search for non-deterministic render inputs

Search is faster than inspecting every component manually. This query finds common inputs; each result is a lead, not automatically a bug.

rg -n --glob '*.{ts,tsx,js,jsx}' \
  'Date\(|Date\.now|Math\.random|randomUUID|localStorage|sessionStorage|window\.|document\.|navigator\.|matchMedia|toLocale' \
  app components pages

For every hit, ask one question: Can this expression change the returned JSX before useEffect runs? If yes, either make it a stable prop or defer it.

Step 3: Make time and locale output stable

This minimal example fails when server and browser clocks, timezones, or locale defaults differ:

"use client"
 
export function OrderPlacedAtBad() {
  return <time>{new Date().toLocaleString()}</time>
}

Do not assume that one deployment region means one timezone. A Docker image may be configured for UTC while a visitor uses India Standard Time; different Node builds can also carry different ICU locale data.

Pass a value that belongs to the request or database record. Render the same stable fallback on both sides, then format for the visitor after hydration:

"use client"
 
import { useEffect, useState } from "react"
 
type EventTimeProps = {
  iso: string
}
 
export function EventTime({ iso }: EventTimeProps) {
  const [label, setLabel] = useState(iso)
 
  useEffect(() => {
    const formatter = new Intl.DateTimeFormat(undefined, {
      dateStyle: "medium",
      timeStyle: "short",
    })
 
    setLabel(formatter.format(new Date(iso)))
  }, [iso])
 
  return <time dateTime={iso}>{label}</time>
}

The server and initial client render both output the exact iso string. The effect runs only in the browser and changes it to the visitor's local format. The tradeoff is a small text update after hydration. For highly visible times, reserve enough width or display a neutral skeleton so the update does not cause layout shift.

Step 4: Read browser storage and media state after hydration

This is a common incorrect fix. It avoids calling localStorage on the server but still chooses a different first render in the browser:

"use client"
 
import { useState } from "react"
 
export function ThemeLabelBad() {
  const [theme] = useState(() => {
    if (typeof window === "undefined") return "light"
    return window.localStorage.getItem("theme") || "light"
  })
 
  return <p data-theme={theme}>Theme: {theme}</p>
}

Use a common default, then update it in an effect:

"use client"
 
import { useEffect, useState } from "react"
 
type Theme = "light" | "dark"
 
export function ThemeLabel() {
  const [theme, setTheme] = useState<Theme>("light")
 
  useEffect(() => {
    const stored = window.localStorage.getItem("theme")
    setTheme(stored === "dark" ? "dark" : "light")
  }, [])
 
  return <p data-theme={theme}>Theme: {theme}</p>
}

Expected initial HTML and initial client output:

Theme: light

After hydration, a saved dark preference changes the label to Theme: dark without a mismatch.

This produces a flash if the preference controls the entire page theme. For that case, use a theme solution designed for Next.js, or set an intentional pre-hydration document class with a matching strategy. Do not broadly suppress the warning on the document as a substitute for making child components deterministic.

For responsive layout, prefer CSS media queries so both sides render the same DOM. A JavaScript branch on matchMedia() that chooses a mobile navigation or a desktop navigation before hydration is fragile. Use an effect only when browser capability changes behavior, not merely styling.

Step 5: Use useId or data IDs, never random rendered IDs

This code creates different htmlFor and id values in the two environments:

export function EmailFieldBad() {
  const id = "email-" + Math.random()
 
  return (
    <div>
      <label htmlFor={id}>Email address</label>
      <input id={id} type="email" autoComplete="email" />
    </div>
  )
}

For an accessibility relationship local to a component, React's useId is the correct tool:

"use client"
 
import { useId } from "react"
 
export function EmailField() {
  const id = "email-" + useId()
 
  return (
    <div>
      <label htmlFor={id}>Email address</label>
      <input id={id} type="email" autoComplete="email" />
    </div>
  )
}

useId derives IDs from the component's position in an identical tree, which makes it safe for server rendering. It is not a list-key generator and cannot rescue a component tree that itself changes between server and client. See the React useId reference for the multi-root identifierPrefix requirement when separate React applications share one page.

Step 6: Repair invalid HTML before blaming React

Browsers parse invalid HTML according to their own rules. For example, a paragraph cannot contain a div; the browser may implicitly close the paragraph before the div. React then attempts to hydrate a DOM shape that is not the one its JSX describes.

export function ReleaseNoteBad() {
  return (
    <p>
      Release notes:
      <div>Deployment completed.</div>
    </p>
  )
}

Use a valid structure:

export function ReleaseNote() {
  return (
    <section>
      <p>Release notes:</p>
      <div>Deployment completed.</div>
    </section>
  )
}

Also check nested interactive elements, such as a button inside a button or an anchor inside an anchor. The Next.js hydration error guide lists invalid nesting as a first-class cause because parser repair happens before React can reconcile the tree.

Step 7: Keep request data as one initial snapshot

An authenticated header, experiment variant, locale, feature flag, inventory count, or currency should have one source for the server response and first client render. A frequent production bug is:

  1. the server renders from the request cookie;
  2. the client immediately renders from a stale localStorage cache; and
  3. the page starts with two different users or variants.

Pass the server's initial snapshot as a serializable prop. The client can revalidate after hydration, but it should not replace the first render with a different cache value. This is also a security boundary: do not let a client-controlled display cache decide authorization-sensitive server content.

Choose the Right Repair, Not the Shortest One

StrategyBest forBenefitCost or caveat
Stable server propRequest data, timestamps, flags, authenticated UICorrect SSR, SEO, and no mismatchRequires clear data ownership
useEffect after stable fallbackStorage, browser APIs, locale formattingKeeps SSR and makes browser-only read explicitSecond paint or possible flash
CSS media queryResponsive presentationSame DOM, no JavaScript raceCannot replace behavior that truly needs a browser API
useIdLabels, descriptions, local accessibility IDsStable SSR-safe identifiersNot for list keys or database IDs
dynamic(..., { ssr: false })Maps, editors, device SDKs that cannot render on serverAvoids SSR for a truly browser-only islandLess initial HTML; worse loading and SEO if overused
suppressHydrationWarningOne unavoidable text or attribute leafSilences a known narrow warningOne level only; does not repair mismatched text

When client-only dynamic import is appropriate

Some libraries require a real browser, WebGL, a browser extension API, or a third-party widget that touches document at module load. Isolate that widget; do not turn an entire route into a client-only page.

"use client"
 
import dynamic from "next/dynamic"
 
const LiveMap = dynamic(() => import("./live-map"), {
  ssr: false,
  loading: () => <p aria-live="polite">Loading map…</p>,
})
 
export function StoreMap() {
  return <LiveMap />
}

In the App Router, put this pattern in a Client Component. Keep the surrounding address, description, and primary content server-rendered. Next.js documents the ssr: false option in its lazy-loading guide; it is a rendering tradeoff, not a general hydration fix.

When suppressHydrationWarning is defensible

For a single inherently local leaf, such as a timestamp where an ISO fallback is unacceptable, this can be a deliberate exception:

"use client"
 
type LocalClockProps = {
  iso: string
}
 
export function LocalClock({ iso }: LocalClockProps) {
  return (
    <time dateTime={iso} suppressHydrationWarning>
      {new Date(iso).toLocaleString()}
    </time>
  )
}

Use it only after you have confirmed that the surrounding tree and semantics are identical. React limits suppression to one level and does not try to patch the mismatched text. It is unsuitable for layout, navigation, forms, auth state, or anything users can interact with before a later update.

Debugging Matrix for Real Deployments

EnvironmentMismatch you may only see thereWhat to verify
Linux or DockerContainer timezone, locale, ICU data, environment flagsPin server rendering to stable data; test UTC and a visitor timezone
macOS or WindowsDeveloper locale hides a time or number formatting issueUse explicit locale and timezone in tests when output must be fixed
WSLDifferent Node binary, environment variables, or filesystem case behaviorReproduce with the exact Node version and production build command
CI/CDBuild-time data or a feature flag changes between build and requestKeep dynamic request data out of statically baked markup unless intended
CDN / edgeHTML minification, injected scripts, response rewritingDisable HTML rewriting for the route and compare origin versus edge response
iOS SafariAutomatic phone, date, email, or address linkificationAdd the documented format-detection meta tag when linkification changes text
CSS-in-JSServer and client class hashes or style insertion order disagreeUse the framework integration for the exact library and router

For iOS linkification, add the tag in the document head when your content must not be rewritten into links:

import type { ReactNode } from "react"
 
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta
          name="format-detection"
          content="telephone=no, date=no, email=no, address=no"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

For CSS-in-JS, do not hand-copy a fix for a different library. Styled Components, Emotion, and other systems have different SSR registries and compiler settings. Use the official Next.js CSS-in-JS guidance for your library and router.

Verification Steps

Use this checklist before closing a hydration bug:

  1. Run npm run build followed by npm run start; do not verify only with the development server.
  2. Open the route in a private window with all extensions disabled.
  3. Reload with the DevTools console open. There should be no hydration warning or recoverable hydration error.
  4. Compare View Source to Elements near the affected component. Their initial node hierarchy and meaningful attributes should agree.
  5. Test a saved browser preference, a different locale, and a different timezone when the component formats data.
  6. Test the deployed CDN path, not only the origin or localhost route.
  7. Exercise the affected control after hydration. A warning-free page that loses input state or has a broken label relationship is not fixed.

For high-value routes, make hydration warnings a browser-test failure. This complete Playwright example captures console messages for one route:

import { expect, test } from "@playwright/test"
 
test("account page hydrates without a mismatch", async ({ page }) => {
  const hydrationMessages: string[] = []
 
  page.on("console", (message) => {
    const text = message.text()
    if (
      /hydration|initial UI does not match|text content did not match/i.test(
        text
      )
    ) {
      hydrationMessages.push(text)
    }
  })
 
  await page.goto("/account", { waitUntil: "networkidle" })
 
  expect(hydrationMessages).toEqual([])
  await expect(
    page.getByRole("heading", { name: "Your account" })
  ).toBeVisible()
})

Run the test against a production build in CI. If a browser extension is part of your developer setup, it will not be present in the test browser, which helps distinguish an application error from local DOM injection.

Prevention

Render policy checklist

  • Treat a component render as a pure function of stable props until hydration completes.
  • Keep current time, random generation, and browser reads out of rendered JSX.
  • Serialize request-derived values once on the server and use them as initial client props.
  • Prefer CSS over window.innerWidth or matchMedia for responsive layout.
  • Validate semantic HTML, especially around paragraphs, lists, tables, forms, anchors, and buttons.
  • Use useId for id, htmlFor, aria-describedby, and similar local relationships.
  • Configure a compatible server and browser setup for each CSS-in-JS library.
  • Review CDN auto-minify, HTML rewrite, translation, and injection features before enabling them globally.

Performance and product tradeoffs

Deferring browser-only work to an effect is usually the right correctness choice, but it adds a second render. Keep that deferred surface small. A locale-formatted timestamp is inexpensive; deferring an entire product page causes a visible layout change and weakens the reason to use SSR in the first place.

Likewise, ssr: false avoids a mismatch by opting out of server rendering for a component. It is a good boundary for a map or chart that cannot render on the server. It is a poor default for navigation, product content, or forms that should be visible, indexable, and usable as soon as HTML arrives.

Common Mistakes

MistakeWhy it failsBetter move
Adding typeof window directly to JSX branchingServer and browser can select different branchesRender one fallback and change state in useEffect
Deleting the warning with a root suppressHydrationWarningIt hides evidence while children can still differIdentify the exact leaf and repair the source
Turning every component into ssr: falseRemoves useful SSR and can harm loading behaviorIsolate only a browser-only dependency
Setting a fixed server timezone to match one developerVisitors still have different local zonesRender stable data first, then localize after hydration
Using a global incrementing ID counterRequest order and hydration order can differUse useId or a persisted data ID
Assuming production is only minified devCDN, edge, locale, cookies, and CSS pipeline can differTest a production build and deployed response

Official References

FAQs

How do I fix Hydration failed because the initial UI does not match in Next.js?

Make the server HTML match the component's first browser render. Remove Date, Math.random, localStorage, window, matchMedia, and locale-dependent output from render, or pass a stable server value. Read browser-only data in useEffect after hydration. Also repair invalid HTML nesting and test without browser extensions or CDN HTML transforms.

Why does a use client component still cause a Next.js hydration error?

"use client" enables client-side interactivity, but Next.js can still render the component's initial HTML on the server. The first browser render must produce the same output. Reading storage or window during render can give the server a fallback and the browser a different value, causing a mismatch.

Does suppressHydrationWarning fix a hydration mismatch?

No. It suppresses React's warning for one element level when a small text or attribute difference is unavoidable, such as a local timestamp. It does not repair a mismatched tree, and React does not patch mismatched text there. Do not use it to hide auth state, invalid markup, navigation, or form differences.

Can a browser extension cause a React hydration error?

Yes. Password managers, grammar tools, translators, dark-mode tools, and ad blockers can add attributes or nodes before React hydrates. Reproduce in a clean browser profile or private window with extensions disabled. If the error disappears, keep the app markup valid and avoid treating extension-only DOM changes as an application bug.

Why does a hydration error happen only in production?

Production may use different request data, locale, timezone, CDN or edge transforms, CSS extraction, environment variables, and minification than local development. Run next build and next start, compare the raw response with the parsed browser DOM, and test the deployed URL with extensions disabled.

Should I disable SSR to get rid of a hydration error?

Only for a genuinely browser-only island, such as a map, editor, or device SDK that cannot render on the server. Disabling SSR avoids the contract by removing the server markup, but it also removes useful initial HTML and can worsen loading, SEO, and perceived performance. Prefer a deterministic first render for ordinary content and controls.

Key takeaways

  • Hydration is a contract: React expects the initial browser render to match the server HTML, not merely look similar.
  • A Next.js Client Component still participates in the initial server render, so useEffect—not a typeof window branch in render—should read browser-only state.
  • Dates, locale formatting, random IDs, localStorage, matchMedia, invalid nesting, browser extensions, and CDN rewrites are common mismatch sources.
  • Compare View Source with the browser's parsed Elements DOM to separate application output from parser or external mutations.
  • Use useId for accessibility IDs and stable server props for request-specific data; do not use Math.random for rendered IDs.
  • suppressHydrationWarning is a one-level escape hatch for an unavoidable leaf mismatch. It does not repair the DOM or make a broken tree safe.

Frequently asked questions

How do I fix Hydration failed because the initial UI does not match in Next.js?

Make the HTML emitted by the server match the component's first browser render. Remove Date, Math.random, localStorage, window, matchMedia, and locale-dependent output from render, or pass a stable server value. Read browser-only data in useEffect after hydration. Also repair invalid HTML nesting and test without browser extensions or CDN HTML transforms.

Why does a use client component still cause a Next.js hydration error?

use client marks a component as interactive and allows hooks, but Next.js can still render its initial HTML on the server. Its first browser render must therefore produce the same output. Reading localStorage or window during render can give the server a fallback and the browser a different value, which creates a mismatch.

Does suppressHydrationWarning fix a hydration mismatch?

No. It suppresses React's warning for one element level when a small text or attribute difference is unavoidable, such as a local timestamp. React documents it as an escape hatch and does not attempt to patch mismatched text there. Do not use it to hide invalid nesting, different component trees, authentication state, or broad document changes.

Can a browser extension cause a React hydration error?

Yes. Password managers, grammar tools, translators, dark-mode tools, and ad blockers can add attributes or nodes before React hydrates. Reproduce in a clean browser profile or private window with extensions disabled. If the error disappears, keep application markup correct and avoid treating extension-only attributes as an application mismatch.

Why does a hydration error happen only in production?

Production can use different request data, time zones, locales, CDN or edge transforms, CSS extraction, environment variables, and minification than local development. Reproduce with next build and next start, then compare the raw response with the parsed browser DOM. Test the deployed path without extensions and inspect any HTML-rewriting CDN feature.


Related Posts