Skip to content
loke.dev
Two route render paths that should produce the same page before hydration

Fix Next.js Hydration Errors in Production Without Guessing

A practical way to find the first server and browser difference behind a Next.js hydration error, with fixes for dates, browser APIs, HTML nesting, and extensions.

Published By Loke3 min read

Hydration errors are annoying because the red message often points at the last component React touched, not the first thing that became different. The useful question is simpler: what did the server render, and what did the browser render on its first pass?

If those two trees are not the same, React has to stop and complain. Fix that first difference. Do not start by adding suppressHydrationWarning everywhere.

The short version

Look for values that can change between the server and browser: Date, Math.random, window, localStorage, screen size, browser extensions, and invalid HTML nesting. Move browser-only work into an effect, pass a stable value from the server, or isolate the component from SSR when that is really what you want.

export default function Clock() {
  return <p>{new Date().toLocaleTimeString()}</p>
}

That looks harmless, but the server clock and the browser clock are not guaranteed to match. Even a small difference is enough to produce a mismatch.

Start with the first mismatch, not the whole page

Make a production build and check the browser console from a clean session. Private browsing helps because extensions and saved state can change the result. Then remove half the page until the warning disappears. Add the pieces back until it returns.

This is boring, but it gives you a small reproduction. A small reproduction beats staring at a 1,000 line page component.

Browser APIs need to wait

window, document, localStorage, matchMedia, and navigator do not exist while the server is rendering. A typeof window check inside the returned markup can still produce two different trees, so it is not a complete fix.

'use client'

import { useEffect, useState } from 'react'

export function SavedTheme() {
  const [theme, setTheme] = useState<string | null>(null)

  useEffect(() => {
    setTheme(localStorage.getItem('theme'))
  }, [])

  return <span>{theme ?? 'system'}</span>
}

The first render is the same on both sides. The effect runs later, after hydration, when localStorage exists.

Dates and random values need a stable owner

For a timestamp, calculate it on the server and pass the value down. For a random ID, use React’s useId or create the value outside the render path. Do not use Math.random in JSX and hope the numbers line up.

import { useId } from 'react'

export function Field() {
  const id = useId()
  return (
    <>
      <label htmlFor={id}>Email</label>
      <input id={id} name="email" />
    </>
  )
}

HTML structure can also be the problem

Invalid nesting is easy to miss in a component tree. A paragraph inside another paragraph, a button inside a button, or an anchor inside an anchor can be repaired differently by the browser before React sees it.

Inspect the rendered HTML, not only the JSX. The browser inspector shows what the browser actually built.

Cloudflare, extensions, and other HTML changes

The Next.js docs also call out browser extensions and services that rewrite HTML. If the mismatch disappears in a clean browser but returns in your normal profile, test extensions before changing application code. Turn off HTML minifiers or response rewriting one at a time too.

A production checklist

Check these in order:

1. Reproduce with a production build and a clean browser.
2. Find the first element whose text or attributes differ.
3. Remove Date, random values, and browser APIs from the first render.
4. Validate HTML nesting around that element.
5. Check extensions, CDN transforms, and CSS-in-JS setup.
6. Use a client-only component only when the UI truly cannot render on the server.

Server Componentfetch, secrets, databaseDefault. Keep work and dependencies on the server.
Client ComponentuseState, click, browser APIsAdd 'use client' only at the interactive edge.
Hydration works when the server render and the browser first render meet at the same boundary.

The goal is not to silence React. It is to make the first render deterministic, then add browser-only behavior after the page has hydrated.