Next.js Hydration Error: 6 Causes Tested on Next.js 16
A hydration error means the HTML the server sent and the first render in the browser are different. React notices, throws away that part of the server HTML, and renders it again on the client. The page usually still works, which is why these errors pile up unnoticed, but the work the server did is wasted and the content can flash.
I reproduced six causes on Next.js 16.4.4 with React 19.3.1, each on its own page, with the server running in UTC and the browser in Pakistan time (UTC+5). Then I loaded every page in next dev and in next start and read the browser console.
What I found
cause next dev production
new Date() in a client component error (text) #418 (text)
Math.random() error (text) #418 (text)
typeof window !== "undefined" branch error (text) #418 (text)
<div> inside <p> 2 errors (nesting+HTML) #418 (HTML)
toLocaleString() without a time zone error (text) none here, but see below
useState + useEffect none none
suppressHydrationWarning none none, but stale textnext build passed for every page, including the invalid <div> inside <p>. Hydration errors only show up in the browser, so a green build tells you nothing about them.
What the error looks like in dev and in production
In next dev the console shows the full message and a diff of the component tree. The lines marked + are what the client rendered and the lines marked - are what the server sent:
Error: Hydration failed because the server rendered text didn't match the client.
...
<ClientPageRoot Component={function P} ...>
<P params={Promise} searchParams={Promise}>
<p>
+ 2026-11-09T09:45:59.966Z
- 2026-11-09T09:45:59.751ZIn production the same problem prints only a minified error, with no diff and no component name:
Error: Minified React error #418; visit https://react.dev/errors/418?args[]=text&args[]=
for the full message or use the non-minified dev environment for full errorsargs[]=text means a text node differed; args[]=HTML means an element differed. If you see #418 in production, run the same page in next dev to get the diff that points at the component.
1. Dates and Math.random()
"use client";
export default function P() {
return <p>{new Date().toISOString()}</p>; // server time != browser time
}The server renders one value and the browser renders another a few hundred milliseconds later. Math.random() fails the same way. On a statically generated page it's worse: the server value is the time of the build. Fix it by rendering a placeholder first and setting the real value after mount, which produced no error in either mode:
"use client";
import { useEffect, useState } from "react";
export default function P() {
const [now, setNow] = useState<string | null>(null);
useEffect(() => setNow(new Date().toISOString()), []);
return <p>Now: {now ?? "…"}</p>;
}If the value matters for SEO, compute it on the server and pass it down as a prop instead, so both renders use the same value.
2. typeof window checks
"use client";
export default function P() {
return <p>{typeof window === "undefined" ? "server" : "client"}</p>;
}Client components also render on the server, where window doesn't exist, so this branch is guaranteed to differ. The diff showed + client and - server. Use the useEffect pattern above, or load the component only in the browser:
"use client";
import dynamic from "next/dynamic";
const Clock = dynamic(() => import("../components/Clock"), {
ssr: false,
loading: () => <p>…</p>,
});
export default function P() {
return <Clock />;
}The server HTML had only the loading paragraph, the browser rendered the clock, and the console stayed clean. Remember that anything behind ssr: false is missing from the HTML crawlers get.
3. Invalid HTML nesting
export default function P() {
return <p>outer<div>inner block</div></p>;
}This one was a server component, so no client code was involved at all. The server sent the markup as written, but the browser's HTML parser doesn't allow a <div> inside a <p>: it closes the <p> early, so the DOM no longer matches what React expects. next dev printed two errors: "In HTML, <div> cannot be a descendant of <p>. This will cause a hydration error." and then "Hydration failed because the server rendered HTML didn't match the client." Production printed #418 with args[]=HTML.
The common versions in real code: a component that renders a <div> used inside a <p>, a <a> inside another <a>, a <table> without <tbody>, or a CMS rich-text field rendered inside a <p>. Change the outer <p> to a <div>.
4. Locale and time zone formatting
"use client";
export default function P() {
// a fixed moment: 9 Nov 2026, 23:30 UTC
return <p>{new Date(Date.UTC(2026, 10, 9, 23, 30)).toLocaleString()}</p>;
}Even with a fixed date this failed in dev: the server in UTC rendered 11/9/2026, 11:30:00 PM and the browser in Pakistan rendered 11/10/2026, 4:30:00 AM. In production it didn't fail for me, and the reason is a trap. The page was static, so it was rendered at build time on my machine, which is also on Pakistan time. It only matched because the build machine and my browser shared a time zone. Any visitor in another time zone, or a build on a server in UTC, gets the error.
Pass the time zone (and locale) explicitly so both sides format the same way. This version rendered 11/10/2026, 4:30:00 AM on the UTC server and in the browser, with no error:
new Date(iso).toLocaleString("en-US", { timeZone: "Asia/Karachi" })If the time should be in each visitor's own zone, that can only be known in the browser: format it after mount.
5. suppressHydrationWarning hides the error, not the problem
"use client";
export default function P() {
return <p suppressHydrationWarning>Now: {new Date().toISOString()}</p>;
}The console stayed clean in both modes. But in production the page kept showing the time the page was built, not the current time: React doesn't patch text it was told to ignore. It's meant for one-off values like a timestamp where the server value is acceptable, and it only applies one level deep. It's not a fix for a component that has to show the client value.
6. Browser extensions
React's own message lists extensions that edit the page before React loads, such as password managers, translators and Grammarly, and these often add attributes to <html> or <body>. If the error only happens for you, open the page in a private window with extensions off. If it disappears, it isn't your code.
Checklist
- Read the dev diff: + is the client, - is the server, and the component above it is the one to fix.
- Anything that differs per request or per browser (time, random values, window, localStorage, screen size): render a placeholder, set it in useEffect.
- Dates: pass locale and timeZone to toLocaleString or Intl.DateTimeFormat.
- Check nesting: no <div> in <p>, no <a> in <a>.
- Test with your machine in a different time zone from your server; a static build in your own zone can hide the bug.
- Use suppressHydrationWarning only when showing the server's value is fine.