All articles
Troubleshooting#nextjs#react#hydration#ssr

How to fix "Hydration failed" errors in Next.js and React

"Hydration failed because the server rendered HTML didn't match the client" has four common causes: time and randomness, locale formatting, invalid HTML nesting and browser extensions. Here is how to find which one you have and fix it properly.

FX

Codestria Engineering

Engineering team

6 min read

You open your Next.js app in development and the error overlay shows: Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. In production the message is shorter and links to a minified React error, but the problem is the same. React rendered your component on the server, rendered it again in the browser, and the two results were not identical.

This article explains why that matters, walks through the four causes we see most often, and shows the fixes that actually remove the mismatch rather than hiding it.

Why it happens

Server-side rendering produces HTML so the page appears before JavaScript loads. When the JavaScript arrives, React hydrates that HTML: it renders your component tree again in memory and attaches event handlers to the existing DOM nodes instead of creating new ones. For that to work, the first client render must produce exactly the same markup as the server render.

Anything that makes the two renders differ breaks the assumption. React 19 and Next.js 16 give you a much better diff in development than older versions did, showing the server value and the client value side by side, so read the overlay carefully before guessing. The causes fall into four groups:

  • Non-deterministic values such as Date.now(), new Date(), Math.random() or crypto.randomUUID() evaluated during render.
  • Environment-dependent formatting: toLocaleString(), Intl.DateTimeFormat without an explicit time zone, or reading window, localStorage or navigator during render.
  • Invalid HTML nesting, such as a <div> inside a <p> or an <a> inside another <a>. The browser's HTML parser silently repairs the markup, so the DOM no longer matches what React expects.
  • Browser extensions and third-party scripts that inject attributes or elements before React hydrates.

Step 1: Read the diff and isolate the component

The development overlay points at the first mismatching node and shows a + client line and a - server line. That is usually enough to identify the component. If it is not, open the page in a private window with extensions disabled. If the error disappears, you have an extension problem, not a code problem. If it persists, comment out sections of the tree until it goes away, starting from the component the overlay highlights.

Also view the raw server HTML with View Page Source (not the Elements panel, which shows the repaired DOM). Comparing the source with what the component renders on the client often makes nesting problems obvious.

Step 2: Fix time, randomness and browser-only values

A component that renders new Date().toLocaleTimeString() will almost never match: the server rendered it a few hundred milliseconds earlier, possibly in UTC, while the browser renders it in the user's time zone. The same applies to random IDs and anything read from window.

The correct mental model is: the first client render must equal the server render, and then you can switch to the client value. For values that come from the browser environment, useSyncExternalStore expresses this directly through its third argument, getServerSnapshot, which React uses both on the server and during hydration.

components/local-time.tsx
"use client";

import { useSyncExternalStore } from "react";

function subscribe(callback: () => void) {
  const id = setInterval(callback, 1000);
  return () => clearInterval(id);
}

// Snapshot must return a primitive or a stable reference
const getSnapshot = () => Math.floor(Date.now() / 1000);
// Used on the server AND during hydration, so both renders match
const getServerSnapshot = () => null;

export function LocalTime() {
  const seconds = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

  if (seconds === null) return <span aria-hidden>--:--</span>;
  return <time>{new Date(seconds * 1000).toLocaleTimeString()}</time>;
}

For a one-off value that does not need a subscription, the classic pattern still works: render a placeholder, then set the real value in useEffect, which only runs in the browser after hydration. Use it for things like reading localStorage or detecting a feature.

components/saved-filter.tsx
"use client";

import { useEffect, useState } from "react";

export function SavedFilter() {
  const [filter, setFilter] = useState<string | null>(null);

  useEffect(() => {
    // Runs only in the browser, after hydration has completed
    setFilter(localStorage.getItem("filter") ?? "all");
  }, []);

  return <p>Showing: {filter ?? "…"}</p>;
}

For random IDs used in id, htmlFor or aria-* attributes, use React's useId(), which generates the same value on server and client. Never use Math.random() for these.

Step 3: Make locale and time zone formatting deterministic

Your server most likely runs in UTC with an en-US default locale. Your users do not. toLocaleDateString() without arguments therefore produces different strings on each side, and the mismatch only appears for users outside the server's time zone, which is why it often slips past local testing.

Pass an explicit locale and timeZone to Intl.DateTimeFormat or toLocaleString. If you want to show the user's own time zone, render a neutral format (such as an ISO date or a UTC time) on the server and switch to the local format after hydration using one of the patterns above.

lib/format.ts
const dateFormatter = new Intl.DateTimeFormat("en-GB", {
  dateStyle: "medium",
  timeZone: "UTC",
});

// Same output on server and client, regardless of the runtime's defaults
export function formatDate(iso: string) {
  return dateFormatter.format(new Date(iso));
}

Step 4: Fix invalid HTML nesting

React 19 reports nesting problems explicitly, for example In HTML, `<div>` cannot be a descendant of `<p>`. This will cause a hydration error. The browser closes the <p> early when it parses the server HTML, so the DOM tree React finds is a different shape from the one it rendered.

  • Replace a wrapping <p> with a <div> when it contains block elements or components that render them.
  • Do not put interactive elements inside each other: no <button> in a <button>, no <a> in an <a>. A card that is a link and also contains links needs a different structure.
  • Always include <tbody> when rendering tables; browsers insert it automatically if you leave it out.
  • Watch for UI library components that render a <div> by default and are placed inside text elements.

Step 5: Handle browser extensions and theme scripts

Password managers, grammar checkers and translation extensions add attributes to <html> or <body>, or wrap text nodes. React 19 is more tolerant than earlier versions and skips unexpected tags in <head> and <body>, but attribute changes on those elements still trigger warnings. Theme libraries that set a class on <html> from an inline script before hydration cause the same thing.

This is the legitimate use case for suppressHydrationWarning. Put it on the <html> element (and <body> if needed) in your root layout. It only applies one level deep, to that element's own attributes and text, so it does not hide mismatches in your actual components.

Prevention tips

  • Treat the render function as pure: no time, randomness or browser APIs in the render path of components that are server rendered.
  • Centralise date formatting in one module with an explicit locale and time zone.
  • Run your end-to-end tests with a non-UTC time zone and a non-English locale (for Playwright, set timezoneId and locale) and fail the run on console errors.
  • Lint for nesting issues and review UI library components that render block elements.
  • Use dynamic(() => import(...), { ssr: false }) from a Client Component only for widgets that genuinely cannot render on the server, such as a map or a chart that measures the window.

Key takeaways

  • A hydration error means the first client render differed from the server render; React discards the server HTML for that tree.
  • Most cases come from Date, Math.random, locale formatting, invalid nesting or extensions.
  • Render a stable value first, then switch to the client value with useSyncExternalStore or useEffect.
  • Give every Intl formatter an explicit locale and timeZone.
  • Reserve suppressHydrationWarning for <html>, <body> and intentionally different single elements.

If hydration errors, layout shift or slow interactions are holding back your React app, our frontend engineering team can trace them to the root cause. Tell us what you are seeing.

Frequently asked questions

Is a hydration error just a warning I can ignore?

No. When hydration fails, React throws away the server-rendered HTML for that tree and re-renders it on the client. You lose the performance benefit of SSR, can get layout shift, and in some cases event handlers attach to the wrong elements.

Why does the hydration error only appear for some users?

User-specific environments cause it: a different time zone or locale than your server, or a browser extension that modifies the DOM before React hydrates. Reproduce it in a clean browser profile with the user's time zone and language.

Should I add suppressHydrationWarning to fix it?

Only for a single element whose text or attributes are intentionally different, such as a timestamp or the html element when a theme script sets a class. It works one level deep and hides real bugs if used broadly.

Found this useful? Share it with your team.

LinkedInX

Need help with frontend engineering?

Fast, accessible web apps, dashboards and marketing sites with React, Next.js and Vue.

Learn more