button inside button? React produces HTML the browser will rearrange
before hydration. React then finds a different DOM from the one it rendered.
no-invalid-interactive-nesting flags nested buttons and links, <div> in
<p>, table rows outside <tbody>, and similar markup.
| Rule | hydration-proof/no-invalid-interactive-nesting |
|---|---|
| What it reports | Disallow HTML nesting that the browser repairs while parsing, such as <div> in <p> or <a> in <a> |
| recommended / next | Error |
| strict | Error |
| Server Components | Checked (the markup is the same on both sides) |
| Suggestions | No |
| Options | none |
What it reports
Nesting in JSX that is visible in one component (through fragments, &&,
? :, arrays and .map() callbacks, but not through other components):
<a>in<a>,<button>in<button>,<form>in<form>,<label>in<label>, at any depth;- interactive content inside
<a>or<button>:a,button,select,textarea,label,details,iframe,embed,input(unlesstype="hidden"; a dynamictypeis not reported) andaudio/videowithcontrols; - elements that close an open
<p>inside a<p>:div,p,ul,ol,li,dl,dd,dt,table,h1–h6,section,article,header,footer,nav,aside,main,form,pre,blockquote,hr,figure,figcaption,fieldset,address,details,summary,dialog,hgroup,menu,searchand a few legacy tags. As in the HTML parser, the search stops atbutton,table,td,th,caption,objectandtemplate; <tr>as a direct child of<table>(the browser adds<tbody>), and<td>/<th>as a direct child of<table>,<tbody>,<thead>or<tfoot>(the browser adds<tr>).
The report is on the inner element. Elements inside <svg> and <math> are
skipped, and so is <li> outside a list: a component boundary may put it in
the right place.
Unlike most rules, this rule also checks Server Components: React hydrates the elements a Server Component renders, so invalid nesting there breaks hydration too.
What happens to a button inside button: React vs the HTML parser
React renders the tree as written, but the browser builds the DOM from the server's HTML with the HTML parser, which repairs invalid nesting:
server HTML: <p>Intro<div>Details</div></p>
browser DOM: <p>Intro</p><div>Details</div><p></p>When React hydrates, the DOM no longer has the structure it rendered. React reports the problem and renders the page again on the client:
In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.div cannot be a descendant of p
explains that message. hydration-proof test reports the same problem as
HP3001 (invalid nesting) or
HP3002 (nested interactive elements), with the line in
the server HTML.
Incorrect
function Intro() {
return (
<p>
Welcome
<div className="details">Read more below.</div>
</p>
);
}
function CardLink({ href, onSave }) {
return (
<a href={href}>
<h3>Title</h3>
<button onClick={onSave}>Save</button>
</a>
);
}
function Rows({ rows }) {
return (
<table>
{rows.map((row) => (
<tr key={row.id}>
<td>{row.name}</td>
</tr>
))}
</table>
);
}The rule checks Server Components too, so the same mistake in an App Router
page is reported with the next preset:
export default function Page() {
return (
<button type="button">
Compare plans
<button type="button">Details</button>
</button>
);
}Correct
function Intro() {
return (
<div>
<p>Welcome</p>
<div className="details">Read more below.</div>
</div>
);
}
function CardLink({ href, onSave }) {
return (
<div className="card">
<a href={href}>
<h3>Title</h3>
</a>
<button onClick={onSave}>Save</button>
</div>
);
}
function Rows({ rows }) {
return (
<table>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>{row.name}</td>
</tr>
))}
</tbody>
</table>
);
}Options
This rule has no options.
Messages
What ESLint prints for this rule, word for word:
<<child>>cannot be inside another<<parent>>. The browser repairs this HTML while parsing the server response, so the page no longer has the tree React rendered and hydration fails. Make them siblings or use a different element.- Interactive
<<child>>cannot be inside<<parent>>. The HTML is invalid, React reports it as a hydration error, and browsers handle clicks on it inconsistently. Move it outside the<<parent>>. <<child>>cannot be inside<p>. The browser closes the<p>before the<<child>>while parsing the server HTML, so the DOM no longer matches what React rendered and hydration fails. Use a<div>instead of the<p>, or a<span>instead of the<<child>>.<tr>cannot be a direct child of<table>. The browser inserts a<tbody>while parsing the server HTML, so the DOM no longer matches what React rendered. Wrap the rows in<tbody>,<thead>or<tfoot>.<<child>>must be inside a<tr>. The browser inserts a<tr>while parsing the server HTML, so the DOM no longer matches what React rendered. Wrap the cells in<tr>.
When not to use it
When the HTML is never parsed by a browser before React takes over (pure client rendering). Even then the markup is invalid, so keeping the rule is recommended.
Related
Nesting that crosses component boundaries (<p><Card /></p> where Card
renders a <div>) cannot be seen by a linter. hydration-proof test finds it
in the real server HTML.
- Invalid HTML nesting: the cause and its fixes
- validateDOMNesting warnings, React's development warning for the same nesting
- eslint-plugin-validate-jsx-nesting compared
- HP3002: nested interactive elements