Hydration Proof

Search documentation

Find a page or section

Disallow HTML nesting that the browser repairs while parsing, such as <div> in <p> or <a> in <a>.

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.

Rulehydration-proof/no-invalid-interactive-nesting
What it reportsDisallow HTML nesting that the browser repairs while parsing, such as <div> in <p> or <a> in <a>
recommended / nextError
strictError
Server ComponentsChecked (the markup is the same on both sides)
SuggestionsNo
Optionsnone

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 (unless type="hidden"; a dynamic type is not reported) and audio/video with controls;
  • 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, search and a few legacy tags. As in the HTML parser, the search stops at button, table, td, th, caption, object and template;
  • <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:

app/pricing/page.jsx
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.

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.