Find where the server output and first browser render disagree, then fix that specific difference.
The page loads. Then the console reports a hydration mismatch.
You add a browser check, move something into useEffect, or suppress the warning. Sometimes it disappears. Sometimes it returns on a different device.
A better starting point is to identify exactly what changed between the server-rendered page and the browser’s first render.
What hydration expects
Hydration connects React’s client-side behavior to HTML already generated on the server.
The initial client render needs to match that server output. Later updates are fine: a button can change state, a request can load fresh data, and a clock can advance.
The problem is a difference at the moment React takes over. React explicitly says to treat hydration mismatches as bugs; it does not guarantee that mismatched attributes will be corrected. See the hydrateRoot documentation.
Before changing code, ask:
What value, element, or attribute differs on the first render—and why?
Start with the reported component
Reproduce the error with a direct page reload. Read the hydration message and component stack before investigating unrelated components.
Then narrow the problem:
- Find the smallest component containing the reported difference.
- Replace its dynamic output with a fixed value.
- Reload.
- Restore one input at a time.
For a product card, that might mean temporarily replacing the price, availability, and personalized label separately.
If a fixed price removes the error, investigate the price’s inputs and formatting. You have a smaller problem than “the page has hydration errors.”
Fix browser-only state
This component renders different content depending on where it runs:
export default function SavedItems() {
const count =
typeof window === 'undefined'
? 0
: Number(localStorage.getItem('savedCount') || 0);
return <span>Saved items: {count}</span>;
}
The server might render 0, while the browser’s first render produces 3.
The typeof window check prevents a server-side reference error. It does not make the output match.
For browser-owned state, start with shared initial content, then read storage after mounting:
'use client';
import { useEffect, useState } from 'react';
export default function SavedItems() {
const [count, setCount] = useState(null);
useEffect(() => {
try {
const stored = localStorage.getItem('savedCount');
const parsed = Number(stored ?? 0);
setCount(
Number.isInteger(parsed) && parsed >= 0 ? parsed : 0
);
} catch {
setCount(0);
}
}, []);
return (
<span>
Saved items: {count === null ? '—' : count}
</span>
);
}
The server and initial client render both show the placeholder. The browser updates afterward.
React documents that effects run only on the client. It also explains this approach for displaying different server and client content in its useEffect reference.
There is a visible tradeoff: the placeholder changes after mounting. For an important value the server can already know, pass that value into the component instead.
Stop generating fresh values during rendering
Look for values such as:
<p>{Date.now()}</p>
<p>{Math.random()}</p>
Independent server and browser executions can produce different output.
For a publication timestamp, use the timestamp attached to the article. Do not generate a new “publication time” when rendering it.
A stable initial representation can be simple:
export default function PublishedAt({ publishedAt }) {
return (
<time dateTime={publishedAt}>
{publishedAt}
</time>
);
}
Here, publishedAt should be the same serialized ISO timestamp in both environments.
If you want a reader-local date, keep a stable initial label and localize it afterward:
'use client';
import { useEffect, useState } from 'react';
export default function LocalDate({ iso }) {
const [label, setLabel] = useState(iso);
useEffect(() => {
const date = new Date(iso);
if (!Number.isNaN(date.getTime())) {
setLabel(date.toLocaleString());
}
}, [iso]);
return <time dateTime={iso}>{label}</time>;
}
This example assumes iso stays unchanged for the mounted component. If it needs to represent a different document, reset the component appropriately—for example, by keying it to that document.
Use the same initial data
Suppose the server renders an inventory count of 12. Before hydration, the browser obtains a newer count of 11 and uses that as its initial state.
Both values may be legitimate. They still disagree.
My preferred investigation is to trace the initial snapshot:
- Which response supplied the server’s value?
- Which data initializes the browser?
- Does the client refetch before hydration completes?
- Are sorting, filtering, or feature flags different?
Use the server-provided snapshot for the first client render. Refresh it afterward through the framework or data library’s supported mechanism.
Also examine personalized content. An anonymous server response and a signed-in browser response need an intentional transition, rather than two incompatible initial trees.
Fix invalid HTML
Check the structure around the error:
// Invalid nesting
<p>
Installation instructions
<div>Run the command below.</div>
</p>
Use valid sibling or parent elements:
<section>
<p>Installation instructions</p>
<div>Run the command below.</div>
</section>
Nested links, nested buttons, and block elements inside paragraphs are worth checking. Next.js lists invalid nesting, browser-only APIs, time-dependent rendering, extensions, and HTML transformations among common causes in its hydration error guide.
Inspect what your component library actually renders. A component named Text might produce a paragraph, making an innocent-looking composition invalid.
Give accessibility IDs a stable mechanism
Do not generate a fresh random ID during rendering to connect a label and input.
Use React’s useId:
'use client';
import { useId } from 'react';
export default function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email</label>
<input id={id} name="email" type="email" />
</div>
);
}
React designed useId to work with server rendering when the server and client component trees match. It does not fix an otherwise different tree, and it should not supply list keys. List keys should come from your data. See the useId documentation.
Investigate changes outside React
If you cannot reproduce the error consistently, compare conditions:
| Observation | Next investigation |
|---|---|
| Only one browser profile fails | Test with extensions disabled |
| Only production fails | Compare deployed HTML and infrastructure settings |
| Appears after a UI-library change | Inspect generated markup and styling integration |
| Only one account fails | Compare session-dependent initial data |
| Only certain dates or regions fail | Inspect formatting inputs and environment differences |
These observations narrow the search. They do not establish the cause on their own.
Don’t suppress the entire problem
suppressHydrationWarning is an escape hatch for a narrowly understood difference. It works only one level deep, and React does not patch suppressed mismatched text.
It is unsuitable for hiding a changing component tree, incorrect account state, or broken HTML.
Likewise, disabling server rendering for a component can be appropriate for a browser-only widget. It should be a deliberate boundary decision. Next.js documents component-level SSR disabling as an option, but moving an entire page to client-only rendering can avoid the mismatch while leaving the original data problem unexplained.
Verify the fix
After making the change:
- Reload the exact URL directly.
- Test a production build.
- Repeat the state that triggered the mismatch.
- Check both the console and visible content.
- Exercise the affected interaction.
For the saved-items example, test empty storage, a saved number, invalid data, and unavailable storage.
A useful fix explains why the initial outputs now agree. “The warning disappeared” is an observation. “Both environments start with the same placeholder, and storage is read after mounting” explains the mechanism.
Sources
About the author
- We are passionate about a couple of things. At first, - we want to make the internet better. Better by being compliant, better UX, Better UI, Better Content, yes - and this results in better understanding how Search Engines work in the World of AI.
Comments
Join the discussionNo comments yet. Be the first to comment.
HTML Delivery
HTML Delivery is about the technical aspects of Delivering websites to a browser. Why does it work like this? Is it Crawlable? How do LLMs see my Website?
