← All articles
🐛 Debugging guideIntermediate · 9 min read

Fixing "Hydration Mismatch" Errors in Next.js

A working method for the most common — and most badly explained — error in modern React applications.

By Kajal Pansuriya · Reviewed by Kishan Vaghani · Published May 25, 2026

The console warning reads: Text content does not match server-rendered HTML. Most fixes online are a copy-paste shrug. This guide walks through what the message actually means, the four causes that account for almost every real case, and the small set of fixes that resolve each.

What hydration is doing

When Next.js serves a page, the server runs your React components and emits HTML. The browser displays that HTML immediately — fast first paint, no waiting for JavaScript. Then React loads, runs the same components again in the browser, and walks the existing DOM attaching event listeners and restoring state. That second walk is hydration. The contract React relies on is that the server-rendered HTML and the first client render produce identical output. If they do not, React cannot match its component tree to the DOM and the page either re-renders from scratch (visible flicker) or breaks subtly.

The mismatch warning is the diagnostic React emits when it spots the first divergence. The location it reports is rarely the root cause — it is the first element where the trees differ, which is usually some distance below the input that actually diverges.

The four causes that account for almost everything

1. Reading window, document, or browser-only globals during render

On the server, window is undefined. Code that reads it tends to be defensively wrapped with typeof window !== "undefined", which produces a falsy branch on the server and a truthy branch in the browser. That is the mismatch — same component, different output, broken contract.

// Broken — diverges between server and client
function Greeting() {
  const dark = typeof window !== "undefined"
    && window.matchMedia("(prefers-color-scheme: dark)").matches;
  return <span>{dark ? "Hi there 🌙" : "Hi there ☀️"}</span>;
}

Fix it by deferring the browser-only read into useEffect and starting with a server-safe initial value. The first paint shows the safe value; the effect runs after hydration and updates state on the client only.

function Greeting() {
  const [dark, setDark] = useState(false); // safe initial value
  useEffect(() => {
    setDark(window.matchMedia("(prefers-color-scheme: dark)").matches);
  }, []);
  return <span>{dark ? "Hi there 🌙" : "Hi there ☀️"}</span>;
}

2. Date and time values

The server renders at one instant in one timezone. The browser renders at a different instant, in the user's timezone. Any component that calls new Date(), Date.now(), or toLocaleString() during render will produce mismatched output. The fix is the same as above — compute the time on the client after hydration, with a neutral placeholder during the first paint.

function Timestamp({ iso }) {
  const [local, setLocal] = useState("");
  useEffect(() => {
    setLocal(new Date(iso).toLocaleString());
  }, [iso]);
  return <time dateTime={iso}>{local || "—"}</time>;
}

3. Random values used in render output

A component that calls Math.random() or generates a UUID inline will produce different values on each render. Server renders one set; client renders another. The fix is to generate the value once, outside the render path, and pass it in as a prop or pull it from a stable source (a database, a server action, the URL).

4. localStorage, sessionStorage, and other client-only state

The classic dark-mode-toggle bug. A component reads localStorage.getItem("theme") during render to decide which CSS class to apply. The server has no localStorage so it renders the default theme. The browser reads the saved value and renders the other theme. Mismatch every time.

The fix has two parts. First, gate the read on a mounted flag so the first client render still matches the server. Second, accept that there will be a one-frame flicker as the saved theme replaces the default — and either accept that flicker, or move the theme decision into a blocking inline script that runs before React loads (the pattern used by every major dark-mode implementation in production).

When suppressHydrationWarning is the right answer

The React team added suppressHydrationWarning as an escape hatch for one specific case: a single element whose content is expected to differ between server and client and which has no good way to be deferred. Timestamps are the canonical example. The escape hatch is intentionally narrow — it suppresses the warning on the element it's applied to and nothing else. Use it sparingly.

<time
  dateTime={iso}
  suppressHydrationWarning
>
  {new Date(iso).toLocaleString()}
</time>

A diagnostic order to follow when the cause is not obvious

  1. Open the React error overlay and identify the component the warning points at. The component is usually somewhere inside the actual cause, not the cause itself.
  2. Walk upward from that component, looking for the first ancestor that reads window, document, a date, a random value, or anything in browser storage.
  3. If nothing obvious shows up, comment out subtrees until the warning disappears. The last subtree you commented out contains the cause.
  4. Once located, fix using the patterns above. Resist the urge to reach for suppressHydrationWarning as the first response.

Related reading

For the broader context on how React server and client rendering fit together, the glossary entry on hydration covers the boundary between Server Components and Client Components. If you're troubleshooting React errors more generally, the JavaScript debugging guide covers the disciplined moves that apply across every kind of frontend bug.

About the writers

Author

Kajal Pansuriya

Developer Educator, ShareCode

Developer educator at ShareCode. Writes the tutorial track — Python, JavaScript debugging, coding-interview prep, and the everyday code-quality habits that hold up in real codebases.

Python fundamentals & teaching beginnersJavaScript debugging & DevToolsCoding-interview preparationClean code & code review
Reviewed by

Kishan Vaghani

Founder & Lead Engineer, ShareCode

Founder of ShareCode. Writes the engineering deep-dives on this site — WebRTC, Firebase Auth, real-time sync, and the production patterns behind the editor itself.

Real-time collaboration & CRDTsWebRTC & low-latency mediaFirebase authentication & security rulesNext.js & full-stack JavaScript

Hit a stubborn hydration bug?

Paste the failing component into a code space, share the link with a teammate, and walk through the diagnostic order together. A second pair of eyes catches the diverging input faster than another hour alone with the warning.

Open a code space