A hydration error means the HTML rendered on the server does not match what React renders in the browser on its first pass. React 19 is stricter than React 18 about this, but it also gives a much better error: a single message with a diff showing the server and client versions.
Read that diff first. It usually points straight at the offending element.
Cause 1: Browser-only APIs during render
Reading window, localStorage or navigator during render produces different output on the server (where they do not exist) and the client.
// Wrong: different on server and client
const theme = typeof window !== "undefined" ? localStorage.getItem("theme") : "light";
// Right: read after mount
const [theme, setTheme] = useState("light");
useEffect(() => setTheme(localStorage.getItem("theme") ?? "light"), []);Cause 2: Time, dates and randomness
new Date(), Date.now(), Math.random() and locale or timezone formatting produce different values on the server and the client.
- Format dates on the server and pass the string down, or
- Render them in a client-only component after mount, or
- Use a fixed timezone and locale when formatting.
Cause 3: Invalid HTML nesting
Browsers repair invalid HTML before React hydrates, so the DOM no longer matches. Common examples:
- A
<div>inside a<p>. - A
<p>inside another<p>. - An
<a>inside another<a>. - Table rows without
<tbody>.
Cause 4: Different data on server and client
If the client fetches its own data during the first render, or the data changed between server render and hydration, the output differs. Pass the server-fetched data down as props so both sides render the same thing.
Cause 5: Browser extensions
Extensions for password managers, translation or grammar checking inject attributes and elements into the page. React 19 tolerates many of these, but if the error only happens for some users, test in a private window with extensions disabled.
Cause 6: Partial prerendering and streaming
With Next.js prerendering a static shell, dynamic data rendered outside a Suspense boundary can mismatch. Wrap dynamic sections in <Suspense> with a fallback so the static and dynamic parts are clearly separated.
When suppressHydrationWarning is OK
Use it only on a single element whose content is expected to differ, such as a timestamp. Do not put it on a container to silence errors; it hides real bugs and only works one level deep.
<time suppressHydrationWarning>{new Date().toLocaleTimeString()}</time>Debugging checklist
- Read the diff in the error and find the element.
- Search that component for
window,Date,Math.randomand locale formatting. - Validate the HTML nesting around it.
- Test with extensions disabled.
- Move client-only logic into
useEffector a client-only component.
Key takeaways
- Hydration errors mean server and client rendered different HTML.
- Browser APIs, time and invalid nesting cause most of them.
- React 19's diff shows exactly where the mismatch is.
- Fix the divergence instead of suppressing the warning.