React 18 + Next.js: Hydration failed because the initial UI does not match what was rendered on the server
After upgrading to React 18 in Next.js I keep seeing this in the browser console:
Hydration failed because the initial UI does not match what was rendered on the server.
SSR is enabled. There’s a shared layout, and in it a typography component (by default it wraps content in a paragraph). I pass “cards” into it, sometimes with tables and other block-level containers.
There’s also a component that reads the window width during render and picks one of two markup variants (basically “mobile/desktop”). Plus there’s a bit on the page that shows the “current time” and generates new IDs for a list.
If I look at the page source, the structure is one thing, but after load the DOM tree in DevTools looks different.
Where should I look first? What usually breaks hydration in this setup?
Answers
Lukas Weber
(Edited)
Your description includes “reads window width during render”. That’s almost guaranteed to cause a mismatch.
On the server there’s no window width, so one branch renders. On the client another branch renders immediately — and React can’t hydrate the existing DOM cleanly.
You need the first client render to match what the server produced. Anything depending on window/document should run after hydration (e.g., in an effect), or the whole chunk should be client-only if it can’t work any other way.
Michael Hoffmann
(Edited)
Agree about window width. Even if you think “it’s just a couple of conditions” — it’s still different HTML on server vs client.
What I usually do: until I have window info, render a neutral variant, then switch after hydration. Yeah, there can be a small “jump”, but no warning and the tree stays consistent.
Thomas Muller
(Edited)
Just in case, I also removed reading window width from render. Now the first render is the same, and switching happens only after the client has initialised.
The warning was already gone after fixing the markup, but I kept this change so it doesn’t come back later.
Hannah Wagner
(Edited)
Often it’s just this: table/rows/cells aren’t built with the right tags, the browser rewrites it. Then hydration fails.
Sabine Schneider
(Edited)
Sounds like invalid HTML structure because of the paragraph wrapper.
If your typography component renders a paragraph, and inside it you have a table / block-level container, the browser will “fix” the HTML while parsing it: it closes the paragraph early and moves elements around. The DOM on the client is then not what React thinks it’s hydrating.
Quick check: compare the page source with the DOM before/after hydration in the problematic area. If you can see the paragraph getting split or the table “jumping out”, that’s it.
Fix: don’t use a paragraph as a universal wrapper for everything. For blocks/tables, use a block wrapper, and make the table structure valid.
Anja Fischer
(Edited)
Yep, this is a classic with typography components: “paragraph by default”, and then someone drops a card with a table inside.
If you don’t want to rewrite a lot, usually it’s enough to change just the wrapper: make the typography wrapper a block element in that spot, not a paragraph. And separately check the table isn’t ending up inside a paragraph via some intermediate wrappers.
Thomas Muller
(Edited)
Found it. It really was the paragraph wrapper: the typography component wrapped a card, and the card contained a table. The browser rearranged the HTML, then React complained.
Changed the wrapper to a block element and tweaked the table structure a bit. The warning went away immediately.
Felix Becker
(Edited)
Check the “current time” and anything random. If it’s computed during render, server and client will almost certainly produce different trees.