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>.

When you nest a button inside button, React renders the tree as written, but the browser's HTML parser repairs the invalid nesting when it reads the server HTML, so the DOM React hydrates is not the one it rendered. no-invalid-interactive-nesting reports <button> in <button>, <a> in <a>, <div> in <p>, table rows outside <tbody> and similar nesting.

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, h1h6, 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.