An invalid HTML nesting hydration error happens when the server sends markup
that HTML does not allow, such as a <div> inside a <p> or a link inside a
link. The browser's parser repairs it while reading the page, so the DOM no
longer has the structure React rendered. Fix the nesting so the parser has
nothing to rewrite.
Symptoms
React names the elements, in development builds only:
In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.
In HTML, <tr> cannot be a child of <table>.
<a> cannot contain a nested <a>.
Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>.
Hydration failed because the server rendered HTML didn't match the client.Production builds show only the generic hydration failure, which is why this cause is easy to miss there. The React-specific messages have their own pages: div cannot be a descendant of p and validateDOMNesting.
hydration-proof reports it as HP3001 (invalid nesting) or HP3002 (an interactive element inside another), with the cause Invalid HTML nesting and the line in the server HTML. It compares the raw server HTML with what the browser's own parser built, so it names this cause with 97% confidence.
Why an invalid HTML nesting hydration error happens
React renders the tree exactly as you wrote it. The browser builds the DOM from the server's HTML with the HTML parser, which follows the HTML rules and fixes what breaks them:
server HTML: <p>Intro<div>Details</div></p>
browser DOM: <p>Intro</p><div>Details</div><p></p>When React hydrates, it expects a <div> inside the <p> and finds it next to
it instead. The common repairs:
| You render | The browser builds | Fix |
|---|---|---|
<div> (or <ul>, <h2>, <table>, another <p>) inside <p> | Closes the <p> before the block, and adds an empty <p> after it | Use <span> inside, or make the outer element a <div> |
<a> inside <a> | Closes the outer link before the inner one | Move the inner link out |
<button> inside <button>, <form> inside <form> | Closes the outer button; ignores the inner form tag | Make them siblings |
<tr> directly inside <table> | Adds a <tbody> around the rows | Render the <tbody> yourself |
<td> directly inside <tbody> or <table> | Adds a <tr> | Wrap cells in <tr> |
How to fix it
Use an inline element inside a paragraph
// Before: <p>Welcome<div className="details">Read more below.</div></p>
export function Intro() {
return (
<div>
<p>Welcome</p>
<div className="details">Read more below.</div>
</div>
);
}Watch components that render a block element: <p><Card /></p> breaks if
Card returns a <div>. Markdown renderers and CMS rich text often wrap
content in <p>, so a component placed inside that content needs to render
inline elements.
Take nested links and buttons apart
A card that is a link and also has a button inside is two interactive elements in one. Make them siblings, and stretch the link over the card with CSS if the whole card must be clickable:
export function CardLink({
href,
onSave,
}: {
href: string;
onSave: () => void;
}) {
return (
<div className="card relative">
<a href={href} className="after:absolute after:inset-0">
<h3>Title</h3>
</a>
<button onClick={onSave} className="relative z-10">
Save
</button>
</div>
);
}Write the table elements the browser would add
export function Rows({ rows }: { rows: { id: string; name: string }[] }) {
return (
<table>
{/* Before: <tr> directly inside <table> */}
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>{row.name}</td>
</tr>
))}
</tbody>
</table>
);
}Stray whitespace between table tags is a text node the table cannot hold.
Keep {" "} and line-broken text out of <table>, <tbody> and <tr>.
Catch it with ESLint
no-invalid-interactive-nesting
reports every pattern in the table above when it is visible in one component,
through fragments, conditionals and .map() callbacks. It checks Server
Components too, because React hydrates the elements they render.
npm install -D eslint-plugin-hydration-proofNesting across components (<p><Card /></p> where Card renders a <div>)
is invisible to a linter.
Catch it in CI
hydration-proof test finds nesting across components, in content from a CMS
and in HTML injected with dangerouslySetInnerHTML, because it compares the
real server HTML with the DOM the browser's parser built, using React's own
nesting rules. It reports HP3001 or HP3002 with the element and its line in the
server HTML. No probe is needed: the parser's repair is the proof. See
how it works.
npx hydration-proof test