Next.js Hydration Errors: Causes and How to Fix Them

Photo by Bernd Dittrich on Unsplash
It means the HTML Next.js rendered on the server did not match what React produced during the first render in the browser. React hydrates by attaching handlers to existing server HTML, so if the client tree differs, it cannot trust the markup, warns in development, and falls back to slower client rendering for that subtree.
The usual causes are invalid HTML nesting the browser repairs, browser-only APIs like window or localStorage used during render, time or locale dependent output such as the Date constructor and number formatting, random values like Math.random, and third-party interference from browser extensions or CDN minification.
Render a stable placeholder on the server and set the real value inside useEffect, which runs only in the browser, so the server and first client render match. For a single unavoidable timestamp element you can instead add suppressHydrationWarning, but use it sparingly as an escape hatch.
Only when a single element's text or attribute is genuinely unavoidable to match, such as a timestamp. It works one level deep and stops React from patching the mismatched text, so never wrap whole subtrees with it. For components that cannot render on the server at all, disable SSR with next/dynamic and ssr false instead.
Yes. Extensions can inject or rewrite attributes and markup before React hydrates, producing a mismatch your code did not create. Reproduce the page in a private window with extensions disabled; if the error disappears, an extension is the cause and your components are fine.

Photo by Bernd Dittrich on Unsplash
Key Takeaway
A Next.js hydration error means the HTML rendered on the server did not match what React produced on the first client render. The fix is to make both renders identical, or to defer genuinely client-only values into useEffect so the initial output stays the same on both sides.
Few Next.js errors are as confusing on first sight as hydration failed because the initial UI does not match what was rendered on the server. The page often looks fine, yet the console lights up red and interactivity feels broken. I have hit this many times across App Router projects, and the cause is always the same shape of problem.
In this post I walk through what hydration actually is, why the mismatch happens, a repeatable way to hunt the offending line down, and the correct fixes for each cause. No guesswork, just the patterns the React and Next.js docs actually recommend.
Next.js prerenders your page to HTML on the server. In the browser, React then hydrates that HTML, walking the same component tree and attaching event handlers to the existing DOM instead of recreating it. Hydration only works if the tree React builds on the client produces the same output as the server did.
When the first client render disagrees with the server HTML, React cannot trust the markup. It warns in development and falls back to client rendering for that subtree, which is slower and can attach handlers to the wrong elements. That is why the docs are blunt: treat mismatches as bugs and fix them, do not paper over them.
Almost every hydration mismatch I have debugged traces back to one of a small set of causes. The React and Next.js docs list the same offenders, so once you recognise the shape you can usually name the culprit before opening the component.
The error text alone rarely points at the exact line, so I work through the same checklist every time instead of guessing:
The primary fix is not to silence the warning, it is to make the server and the first client render produce the same output. For values that are genuinely client-only, render a stable placeholder on the server and fill in the real value after mount inside useEffect, which runs only in the browser.
// BEFORE — hydration mismatch.
// The server renders its own clock time; the browser renders a
// different time at hydration, so the initial UIs never match.
export default function LastUpdated() {
return <p>Last updated: {new Date().toLocaleTimeString()}</p>;
}
// AFTER — stable on the server, filled in on the client.
// The server and first client render both output an empty string
// (they match), then useEffect swaps in the real time after mount.
"use client";
import { useState, useEffect } from "react";
export default function LastUpdated() {
const [time, setTime] = useState("");
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <p>Last updated: {time}</p>;
}This is the pattern the Next.js docs recommend for time-dependent and browser-only values. The server and the first client render both output the same empty placeholder, so hydration succeeds, and the effect then updates the DOM a moment later without any mismatch.
Reach for next/dynamic with ssr false when a whole component can never render meaningfully on the server, such as a chart tied to window size. It disables prerendering for that component only, so nothing is emitted on the server to mismatch against.
Sometimes a difference is genuinely unavoidable for a single element, the classic example being a timestamp. For that narrow case React offers suppressHydrationWarning. If a component must not render on the server at all, disabling SSR with a dynamic import is the cleaner option.
// Escape hatch: only for a single element whose value is
// genuinely unavoidable to match, such as a timestamp.
// It patches one level deep only — do not wrap whole subtrees.
<time dateTime="2026-08-01" suppressHydrationWarning>
{new Date().toLocaleDateString()}
</time>
// For content that must not render on the server at all,
// disable prerendering for that component instead:
import dynamic from "next/dynamic";
const ClientOnlyChart = dynamic(() => import("./chart"), {
ssr: false,
});Treat suppressHydrationWarning as an escape hatch, not a default. It works only one level deep, and when set React will not attempt to patch the mismatched text content, so anything below that element is on its own.
Do not wrap a whole subtree in suppressHydrationWarning to make the red console go away. It hides real bugs one level below, and because React stops patching the text, you can ship UI that silently differs between server and client without any warning.
Here is the mapping I keep in my head, matching each common cause to the fix the docs endorse:
| Cause | Why it mismatches | Correct fix |
|---|---|---|
| Invalid HTML nesting | Browser repairs the markup, so the DOM no longer matches the server tree | Fix the nesting so block elements are not placed inside a p or an a inside an a |
| Browser-only API in render | window or localStorage is undefined on the server, so the branches differ | Move the access into useEffect and render a stable placeholder first |
| Date, locale or number formatting | Server timezone or locale differs from the visitor's, so text differs | Format inside useEffect, or use suppressHydrationWarning on a lone timestamp |
| Random or unique values | Math.random and fresh ids produce a different value each render | Generate the value in useEffect, or use a stable id from useId |
| Browser extension or CDN | Third-party code rewrites the HTML before React hydrates it | Confirm in a clean profile; disable CDN auto-minify; it is not your code |
Hydration errors feel mysterious until you internalise the one rule behind them: the server render and the first client render must produce identical output. Once that clicks, every fix follows from asking what could differ for a given node, and reaching for useEffect, a dynamic import, or a targeted suppressHydrationWarning in that order.