A hydration error means the HTML rendered on the server does not match what React renders on the client during its first render. In Next.js, the mismatch can come from invalid HTML, browser-only values, unstable dates or random numbers, or data that changes between rendering and hydration. The lasting fix is to make the initial renders agree—not to silence the warning.
Key Takeaways
- Hydration compares the server-rendered HTML with the client’s initial render. Make the inputs to those renders consistent.
- Fix invalid HTML nesting and avoid reading browser-only state during render.
- Make dates, random values, and server/client data deterministic for the initial render; update them afterward if needed.
- Use
suppressHydrationWarningonly for a specific, unavoidable mismatch. It is an escape hatch, not a repair.
What hydration errors mean
Next.js can send HTML for a page before the browser runs its JavaScript. React then renders the page in the browser and attaches event handlers to that HTML. If the browser’s initial React render produces different markup, hydration can fail or React may report a mismatch.
A useful debugging question is: What value or markup differs between the server render and the browser’s first render? Find that difference before changing the rendering strategy.
1. Fix invalid HTML nesting
Browsers parse and normalize HTML. If your JSX describes invalid nesting, the DOM the browser creates can differ from the structure React expects. For example, a paragraph cannot contain another paragraph or a div.
Problematic JSX:
export default function Message() {
return (
<p>
Status: <p>Ready</p>
</p>
);
}
Use valid elements for the intended structure instead:
export default function Message() {
return (
<div>
<p>Status:</p>
<p>Ready</p>
</div>
);
}
Check the rendered HTML, not just the JSX. Look for nested paragraphs, interactive elements nested inside other interactive elements, and other structures that the HTML parser may rearrange. Fix the markup at its source rather than trying to make React tolerate a different DOM tree.
2. Keep browser-only APIs out of the initial render
window, localStorage, and navigator are available in the browser, not during server rendering. A common mistake is to branch on their availability while rendering:
function Greeting() {
const greeting =
typeof window === "undefined" ? "Hello" : localStorage.getItem("greeting");
return <p>{greeting ?? "Hello"}</p>;
}
Even though the typeof window check avoids a server-side reference error, it can still produce different initial output: the server renders one value, while the browser reads another.
Render the same fallback first, then read browser state in an effect. In the App Router, put this component in a Client Component file:
"use client";
import { useEffect, useState } from "react";
export default function Greeting() {
const [greeting, setGreeting] = useState("Hello");
useEffect(() => {
const savedGreeting = window.localStorage.getItem("greeting");
if (savedGreeting) setGreeting(savedGreeting);
}, []);
return <p>{greeting}</p>;
}
The server render and first client render both show “Hello.” The saved value appears after the effect runs. This may create a brief visible change, so choose a suitable fallback or loading state for your interface. A Client Component can still be prerendered; adding "use client" alone does not make its initial output browser-only.
3. Make dates consistent across server and browser
Rendering the current time directly is unstable: a server render and a browser render happen at different moments. Locale formatting can also differ because the server and browser may use different time zones or locale settings.
For a value that should be fixed when the page is generated, create it once on the server and pass it to the client as a prop. Render a stable representation first:
// Server Component
import UpdatedAt from "./UpdatedAt";
export default function Page() {
const timestamp = new Date().toISOString();
return <UpdatedAt timestamp={timestamp} />;
}
// UpdatedAt.jsx — Client Component
"use client";
export default function UpdatedAt({ timestamp }) {
return <time dateTime={timestamp}>{timestamp}</time>;
}
Both sides receive the same timestamp, and the ISO string is rendered consistently. If you want a visitor-localized display, render a stable initial value and format it after mount, or explicitly provide the same locale and time zone on both sides. Don’t assume the server’s default time zone matches the visitor’s.
For a live clock, initialize with a stable value and update it in an effect or another client-side mechanism. Do not call new Date() during render and expect the result to match across the server and browser.
4. Avoid random values during rendering
Math.random() returns a different value on each call, so using it in rendered output can produce different server and client markup.
Problematic:
function Badge() {
const number = Math.random();
return <span>{number}</span>;
}
If the value belongs to server-generated content, generate it once on the server and pass it through as a prop. If it is only needed in the browser, render a stable placeholder and set the value after mount:
"use client";
import { useEffect, useState } from "react";
export default function Badge() {
const [number, setNumber] = useState(null);
useEffect(() => {
setNumber(Math.random());
}, []);
return <span>{number === null ? "Loading…" : number.toFixed(3)}</span>;
}
The placeholder is identical during server rendering and the first client render. Only generate random values for display when that behavior is appropriate; randomness used for security must come from a suitable cryptographic source, not Math.random().
5. Keep server and client data in sync
A mismatch can occur when the server renders one data snapshot but the browser fetches or calculates another before its first render. For example, a Client Component that independently fetches changing data on mount may initially render a loading state or a newer value instead of the content the server sent.
When data is available to the server, render from that data and pass the same initial snapshot to the Client Component:
// Server Component
import Profile from "./Profile";
export default async function Page() {
const profile = await getProfile();
return <Profile initialProfile={profile} />;
}
// Profile.jsx — Client Component
"use client";
export default function Profile({ initialProfile }) {
return <p>{initialProfile.name}</p>;
}
Ensure the props passed to a Client Component are serializable under the framework’s rules. If the data must refresh in the browser, use the server-provided snapshot for the initial render and then update it deliberately. Also check whether time-sensitive records, personalization, or cache behavior can cause the server to return different data between requests.
A practical debugging workflow
- Read the mismatch details. Identify the element or text React reports, then trace it to the component that renders it.
- Compare initial values. Check whether the server and first browser render use different time, locale, random values, browser state, props, or fetched data.
- Inspect the actual DOM. Invalid nesting can be corrected by the browser, so compare the browser’s parsed DOM with the intended JSX structure.
- Make the initial output deterministic. Pass a shared value from the server, use valid markup, or render a stable placeholder before applying browser-only changes.
- Recheck the original route and environment. Confirm the warning is gone without suppressing it, and test the affected page with the data and settings that triggered the mismatch.
If the error appears only in a deployed environment, compare runtime data, locale and time-zone defaults, caching, and the HTML delivered by the server. A development-only reproduction is useful, but it may not have the same data or rendering conditions as production.
Why suppressHydrationWarning is not a general fix
React provides suppressHydrationWarning for a narrow case where a particular element’s text or attribute is expected to differ during hydration and the difference cannot reasonably be avoided. For example, a timestamp that must be generated independently on each side may need such treatment.
<time suppressHydrationWarning>{timestamp}</time>
This only suppresses a warning one level deep; it does not make the server and client output match or fix invalid markup. React does not guarantee that mismatched text will be patched when this escape hatch is used. Applying it broadly can hide the clue that reveals a real rendering bug. Prefer stable initial output, and add suppression only to the specific unavoidable element after considering the trade-off.
Common Mistakes
- Assuming
"use client"means a component is never rendered on the server. - Treating
typeof window !== "undefined"inside render as a hydration fix. It can prevent a server exception while still creating different markup. - Calling
new Date()orMath.random()during rendering and expecting identical results. - Fetching separate initial data on the server and client without reconciling the snapshots.
- Adding
suppressHydrationWarningto a parent or many elements instead of correcting the mismatch. - Blaming hydration when the real issue is invalid HTML that the browser has rearranged.
Best Practices
- Keep the server render and first client render based on the same inputs.
- Move browser-only reads and post-mount updates into effects when a brief transition is acceptable.
- Pass server data and stable values to client-rendered components rather than independently recreating them.
- Use valid semantic HTML and inspect the parsed DOM when the mismatch concerns element structure.
- Treat suppression as a last resort for a known, local, unavoidable difference—not as a way to make an unknown error disappear.
Conclusion
To fix a Next.js hydration error, find what differs between the server HTML and the browser’s first render. Correct invalid nesting, stabilize browser-only and time-dependent values, and keep the initial data snapshot consistent. Once the output agrees, React can hydrate it without hiding the underlying problem.
