Skip to content

Next.js error

Hydration Failed Because the Initial UI Does Not Match

The error

Hydration failed because the initial UI does not match what was rendered on the server.

The server rendered one thing, the browser rendered another, and React threw away the server HTML. Here is how to find which node differs, and the four causes that account for almost all of them.

React renders your component tree twice: once on the server, into HTML, and once in the browser, to attach event handlers to that HTML. Hydration is the second pass adopting the first one's output. When the two disagree, React cannot safely adopt it, so it throws the server HTML away and re-renders the subtree on the client.

That is the cost, and it is not cosmetic: the server render you paid for is discarded precisely where the mismatch is.

Find the node before you theorise

The development build names the offending element. Run next dev, reproduce, and read the diff React prints - it shows the server value and the client value. Everything below is a shortcut for interpreting what you find, not a substitute for looking.

If it only reproduces in production, the cause is environmental rather than structural, and the list below narrows to the first two.

The four causes

1. Something that differs between machines. new Date(), Date.now(), Math.random(), crypto.randomUUID(), toLocaleString() without an explicit locale, Intl formatting that reads the system timezone. The server is in UTC in Frankfurt; the browser is in Istanbul at a different millisecond. Any of these rendered directly into markup will mismatch.

The fix is to render the stable form on both sides and adjust after mount:

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
 
// Same output on both passes; the local format arrives after hydration.
return <time dateTime={iso}>{mounted ? formatLocal(iso) : formatUtc(iso)}</time>;

suppressHydrationWarning on that one element is legitimate here and only here - it silences the warning for a single node whose difference you have decided is correct. It is not a general fix and it does not stop the re-render.

2. Browser-only state read during render. window, localStorage, document, navigator, a media query, a theme read from a cookie in client code. The server has none of these, so it renders the fallback and the client renders the real value. Same fix: render the fallback on both passes, read the real value in an effect.

3. Invalid HTML nesting. This one surprises people because the code looks fine. A <div> inside a <p>, a <p> inside a <p>, a <form> inside a <form>, a block element inside <button>. The server serialises exactly what you wrote; the browser's parser silently repairs it into legal HTML before React sees it. The tree React hydrates against is therefore not the tree the server sent, and the mismatch is real even though your JSX is consistent.

Look at the element React names, then check what wraps it.

4. A browser extension editing the DOM. Password managers, translation extensions and accessibility tools inject attributes into <body> and into form fields before React hydrates. If the mismatch is on <body> or an <input>, and only some people can reproduce it, test in a clean profile before changing any code.

What looks like a fix and is not

Wrapping the component in dynamic(..., { ssr: false }). This does stop the error, by not server-rendering the component at all. You have converted a correctness problem into a content-visible-later problem: the markup is no longer in the HTML, so it is absent for a crawler and arrives after hydration for everyone else. Sometimes that trade is right. Choose it deliberately, not to silence a message.

suppressHydrationWarning on a parent. It applies one level deep, so on a wrapper it does nothing useful, and where it does apply it hides the warning without preventing the re-render. You end up with the cost and none of the diagnosis.

Moving everything behind useEffect. Works, and empties your server render. If enough components do it, you have shipped a client-rendered app with extra steps - which is what the client boundary actually costs you.

Why it matters beyond the warning

A mismatch high in the tree re-renders most of the page on the client. That shows up as a slower Largest Contentful Paint and, where the discarded subtree contained your main content, as a difference between what a crawler is served and what a visitor eventually sees.

If you are not sure how much of your tree is affected, that is one of the things an audit measures - and the rendering model underneath it explains what the server was supposed to be producing in the first place.

Related questions

Does this break the page, or is it only a warning?
It breaks more than it looks. React discards the server HTML for that subtree and re-renders it on the client, so you lose the performance of the server render exactly where it mattered, and any state attached to those nodes is reset. In production the message is truncated, which is why it is usually found late.
Why does it only happen in production?
Usually the opposite - it happens in both and only the development build tells you clearly. Development prints the mismatched node; production prints a minified message with an error code. If you cannot reproduce locally, the cause is almost always environmental: a different timezone, a different locale, or data that changed between the two renders.