Skip to main content
How We Debug Hydration Mismatches in the Next.js App Router

How We Debug Hydration Mismatches in the Next.js App Router

July 15, 2026
Frontend Engineering
6 min read

Key takeaways

A hydration mismatch occurs when the HTML rendered by the Next.js server does not match the output of React's first client render, causing React 19 to discard and re-render that subtree in the browser.

The most common causes are non-deterministic values such as Date.now() and Math.random(), browser-only APIs like localStorage and window read during render, invalid HTML nesting, and DOM changes injected by browser extensions.

The suppressHydrationWarning attribute is not a general fix; it silences the warning for a single element whose text is legitimately allowed to differ, such as a timestamp.

To render browser-only values safely, use a mounted flag with useEffect or useSyncExternalStore with a server snapshot so the server and first client render agree.

A hydration mismatch appears only on the first server-rendered load and never on client-side navigation in the App Router.

What is a hydration mismatch, and why does it break the page?

A hydration mismatch is when the HTML Next.js renders on the server does not match the output of React's first render in the browser. During hydration, React walks the existing server DOM and attaches event listeners to it rather than recreating it, assuming its computed tree matches the DOM already on the page. When the two differ, React 19 discards the mismatched subtree and re-renders it from scratch on the client. That re-render is the visible flash our team chases, and it forfeits the server-rendering benefit for that subtree. In development, React 19 now prints a readable diff naming the element and the differing text, a marked improvement over the old generic message.

Why do Date.now() and localStorage cause a hydration error?

Reading a non-deterministic or browser-only value during render is the most common cause we see. Date.now(), Math.random(), and new Date().toLocaleString() produce different output on the server than in the browser, because the server ran a moment earlier and in a different timezone and locale. Reading localStorage, window, or navigator during render is worse, because the server has no such object and renders a different branch than the client. The server stamps the time it rendered while the browser stamps a later time in the user's locale, so React sees two different strings and throws.

When should we use suppressHydrationWarning?

We reach for suppressHydrationWarning only when the text of a single element is legitimately allowed to differ between server and client, and a timestamp is the canonical case. It suppresses the warning for that one element, one level deep; it does not silence its children and it is not a blanket fix. Using it to hide a real logic mismatch simply trades a visible error for an invisible one, so if it spreads beyond a couple of leaf nodes the real problem lies elsewhere.

How do we render a client-only value without a mismatch?

To render a value that exists only in the browser, such as a theme, a viewport width, or a stored preference, we render a stable placeholder on the server and swap in the real value after mount. The two-pass pattern uses a mounted flag: a state value initialised to false that a useEffect sets to true, so both the server and the first client render return the same skeleton and only the second render shows the real value. The cleaner option for external browser state is useSyncExternalStore, whose third argument is a server snapshot that keeps the value defined on both sides. For a component that can never render on the server, next/dynamic with the ssr option set to false skips server rendering entirely.

Why do browser extensions cause hydration errors on the body element?

Browser extensions cause hydration errors by mutating the DOM before React hydrates. Grammarly injects data-gramm attributes, password managers add wrappers, and dark-mode extensions rewrite inline styles on the html and body elements. The server sent clean markup, but by the time React hydrates the DOM carries extra attributes, so they mismatch. Because we cannot control a user's extensions, the accepted fix is to add suppressHydrationWarning to the html and body elements in the root layout, which the Next.js documentation recommends. It is safe there because those elements carry no dynamic text of our own to protect.

How do we fix invalid HTML nesting mismatches?

Invalid HTML nesting produces mismatches that look mysterious because the JSX appears correct. The browser silently repairs illegal nesting by moving or closing tags, so the DOM it builds no longer matches the tree React expects, and React reports a mismatch on an element the developer never touched.

A div inside a p fails because the p auto-closes before a block element, so use a div or span wrapper instead.

A p inside a p fails because a nested p is not allowed and the outer one closes early, so flatten it to a single p.

An anchor inside an anchor fails because interactive elements cannot nest, so restructure the markup so the links are siblings.

The fix is always to make the JSX produce valid HTML, and React 19's diff usually points straight at the reparented node.

FAQ

What is the difference between a hydration mismatch and a rendering error?

A hydration mismatch is specifically the server HTML disagreeing with the first client render, and it appears only on initial load, never on client-side navigation. A rendering error is any exception thrown while rendering, at any time.

Does suppressHydrationWarning fix the flash?

No. It silences the console warning for one element, but React still re-renders the mismatched content on the client. Use it only when the difference is intentional, such as a timestamp.

Why does the error only happen on refresh and not when navigating?

Hydration runs once, on the first server-rendered load. Client-side navigation in the App Router renders entirely in the browser, so there is no server HTML to mismatch against.

Can useEffect cause a hydration mismatch?

No. useEffect runs after hydration, so state set inside it cannot affect the first render that React compares against the server HTML, which is exactly why the mounted-flag pattern is safe.

How do we find which component is mismatching?

Read the React 19 development console diff, which names the element and shows the differing text. If an extension is suspected, reproduce the page in an incognito window with extensions disabled.

How We Debug Hydration Mismatches in the Next.js App Router | Devya Solutions Blog