react: Bug: `validateDOMNesting` Hydration failed

Having an invalid DOM structure, normally triggers validateDOMNesting, but when combined with SSR, this also triggers Hydration failed, and There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

I assume the above is a combination of two things, and error being raised by React, and Next.js not handling it well.

The React Error, as far as I know

React version: 17.0.2, 18.0.0 and 18.1.0.

The current behaviour

In SSR frameworks such as Next.js, an error raises claiming that Hydration failed because the initial UI does not match what was rendered on the server..

With React 18, and Next.js, this, in turn, triggers: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering. I assume, Next.js is somehow not handling the invalidDOMNesting error, or the Hydration failed error, and that is probably something they ought to fix.

Found this bit on the code as well:

  // This validation code was written based on the HTML5 parsing spec:
  // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
  //
  // Note: this does not catch all invalid nesting, nor does it try to (as it's
  // not clear what practical benefit doing so provides); instead, we warn only
  // for cases where the parser will give a parse tree differing from what React
  // intended. For example, <b><div></div></b> is invalid but we don't warn
  // because it still parses correctly; we do warn for other cases like nested
  // <p> tags where the beginning of the second element implicitly closes the
  // first, causing a confusing mess.

  // https://html.spec.whatwg.org/multipage/syntax.html#special

Edit 2023

I’ve now gathered enough info and experience from helping out folks that run into this issue.

First the initial example I had provided is a weak case. Nested <p> tags are assumed by HTML interpreters as a mistake and render them sequentially instead. The react-dom/server rendering behaviour is correct. A structure such as :

<p class="outer">
    <p class="inner">hello</p>
</p>

Is rendered by the browser as:

<p class="outer"> </p>
<p class="inner">hello</p>
<p></p>

However, if you do this programmatically:

const outer = document.createElement('p')
const inner = document.createElement('p')
outer.classList.add("outer")
inner.classList.add("inner")
outer.append(inner)
document.body.append(outer)

Then the DOM is not corrected by the browser and it renders:

<p class="outer"><p class="inner"></p></p>

Another case where the browser, or rather HTML interpreter I guess, changes the received server HTML:

 <table>
   <tr>
     <th>a</th>
     <th>b</th>
     <th>b</th>
   </tr>
   <tr>
 </table>

The above is changed by the browser to:

<table>
  <tbody>
    <tr>
      <th>a</th>
      <th>b</th>
      <th>b</th>
    </tr>
    <tr></tr>
  </tbody>
</table>

However, when React does hydration it’ll render a table without tbody, and that causes a mismatch with what it finds.

Second, while it is annoying, a hydration error produced of invalid DOM nesting is one of the easiest to fix of its kind. The error log often points at where the divergence has occurred!

Third, in the face of validateDOMNesting errors, there are a few things that could help you figure out the error. I often follow this approach:

  • Collect information about the error, what is the error saying it found, and what did it expect?
  • Perhaps you can reproduce it locally?

Catch the browser making changes to the server sent HTML:

  • Inspect the HTML sent by the server, for example in view-source:https://your-site.your-domain, save this and take it to a text editor
  • Disable JavaScript and load your page, does your UI look different?
  • In the Chrome dev tools, with JS disabled, edit as text the entire HTML document, uglify it and take it to a text editor
  • Compare the two pieces of text you’ve saved to text editors

Although the above could probably be automated somehow 😉

Are you making wrong assumptions about how HTML works? One resource I’ve used a lot to avoid this kind of issue is: https://caninclude.glitch.me/

And I think that’s as far as you can go with validateDOMNesting kind of errors. There’s other kind of errors, such as hydration mismatch because you straight up return different DOM, like:

const Trouble = () => typeof window === 'undefined' ? <div>server</div> : <div>client</div>

With that being said I am ready to close this issue.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 5
  • Comments: 20 (5 by maintainers)

Most upvoted comments

This seems to be expected behavior? SSR cannot work with improper tag nesting as per the HTML spec, the browser’s parser won’t accept it, and the whole point of SSR is to have the browser parse the initial document structure for you without using any JS. Client side rendering can work with improper tag nesting because the DOM apis do allow creating these invalid nestings “by hand” (with document.createElement, appendChild, etc.).

There is no way to create a raw HTML document that nests a <p> directly inside a <p> using static markup, so there’s no way to do it with SSR.

So this situation is a warning for client side rendering, an error for SSR, and a warning for hydration; but the only way to continue the “hydration” is to blast away all of the DOM so far because it will be wrong due to invalid nesting.

This is one of the weak points of Next.js. Not communicating the problem well with the developers. Next.js has an extremely poor DX regarding errors.

I also expected it to tell me why the hydration failed and what element it thought was incorrect. In a real-world application, how am I supposed to find the incorrect nesting? I don’t get any warnings in my local development at all. And after deploying the built version to my server, I got this vague error. Now I can’t find what element is nested incorrectly.

Hey! To circle back here, this is a React specific error which logged a couple more lines in the development console. Next.js caught the first part of it and was not something we had control over. Last month @hanneslund on our team dug into this and improved the overlay in a bunch of ways when using the app directory. This is not available in pages yet as we wanted to make sure the approach is stable first but we can backport it for pages.

Notable changes:

  • Component Stack is included for hydration errors, which points at the component where the mismatch happened
  • Additional error messages coming from React are shown, e.g. what text changed
  • Next.js / React stacktraces are collapsed by default
  • Runtime errors (including hydration errors) are collapsed by default, you’ll see a toast at the bottom of the page.

Old

old

New

new

Toast:

CleanShot 2023-02-22 at 13 07 21@2x

I would expect SSR on the server produce a server-console warning that it encountered invalid html nesting and modified html output and a remark that this could cause hydration errors on the client side.

This seems to be expected behavior? SSR cannot work with improper tag nesting as per the HTML spec, the browser’s parser won’t accept it, and the whole point of SSR is to have the browser parse the initial document structure for you without using any JS. Client side rendering can work with improper tag nesting because the DOM apis do allow creating these invalid nestings “by hand” (with document.createElement, appendChild, etc.).

There is no way to create a raw HTML document that nests a <p> directly inside a <p> using static markup, so there’s no way to do it with SSR.

So this situation is a warning for client side rendering, an error for SSR, and a warning for hydration; but the only way to continue the “hydration” is to blast away all of the DOM so far because it will be wrong due to invalid nesting.

Yeah, I mostly agree, and after going through very long discussion threads, it does seem that it is developers fault for not fixing warnings/errors… However, it might be worth having clearer communication with regards to the implications of invalid DOM nesting, maybe it’s already there and I just haven’t seen it.

I’m currently trying to fix this issue by deleting several files at a time from my project until I locate the culprit. There must be a better way to locate the issue?

Would be great to improve this error reporting for validateDOMNesting / Hydration failed! Would be great to get a visual diff of the HTML that should change:

Visual HTML Diff for validateDOMNesting

Improving the existing message / overlay: Showing a visual HTML diff (server rendered HTML vs client rendered HTML) in the error message would be very helpful for creating actionable error messages, as I also wrote in my Next.js issue. Eg something like:

- server
+ client

-<p>
+<p />
  <p>initial</p>
-</p>
+<p />

React providing enough metadata about the HTML related to the error to construct such a diff would be useful for multiple frameworks.

Is a lint or similar that can identify this issue? As others have pointed out, it’s quite difficult without better tooling.

A funny thing about the error is that it appears when the page is translated in development.

Is a lint or similar that can identify this issue? As others have pointed out, it’s quite difficult without better tooling.

Another update on a lint rule, cross-posting my comment from https://github.com/jsx-eslint/eslint-plugin-react/issues/3310#issuecomment-1460653623:


Using some of the original validateDOMNesting code from @sophiebits and the src/mapping.js file from @MananTank’s validate-html-nesting, I was able to come up with a simple set of rules to prevent most footguns for our students using ESLint’s no-restricted-syntax rule (uses @typescript-eslint/utils for the config type at the first line):

(This has also been released as part of @upleveled/eslint-config-upleveled@3.13.0)

.eslintrc.cjs

/** @type {import('@typescript-eslint/utils').TSESLint.Linter.Config} */
const config = {
  rules: {
    'no-restricted-syntax': [
      'warn',
      // Warn on nesting <a> elements, <button> elements and framework <Link> components inside of each other
      {
        selector:
          "JSXElement[openingElement.name.name='a'] > JSXElement[openingElement.name.name=/^(a|button|Link)$/]",
        message:
          'Invalid DOM Nesting: anchor elements cannot have anchor elements, button elements or Link components as children',
      },
      {
        selector:
          "JSXElement[openingElement.name.name='button'] > JSXElement[openingElement.name.name=/^(a|button|Link)$/]",
        message:
          'Invalid DOM Nesting: button elements cannot have anchor elements, button elements or Link components as children',
      },
      {
        selector:
          "JSXElement[openingElement.name.name='Link'] > JSXElement[openingElement.name.name=/^(a|button|Link)$/]",
        message:
          'Invalid DOM Nesting: Link components cannot have anchor elements, button elements or Link components as children',
      },

      // Warn on nesting of non-<li> elements inside of <ol> and <ul> elements
      {
        selector:
          "JSXElement[openingElement.name.name=/^(ol|ul)$/] > JSXElement[openingElement.name.name!='li'][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: ol and ul elements cannot have non-li elements as children',
      },

      // Warn on nesting common invalid elements inside of <p> elements
      {
        selector:
          "JSXElement[openingElement.name.name='p'] > JSXElement[openingElement.name.name=/^(div|h1|h2|h3|h4|h5|h6|hr|ol|p|table|ul)$/]",
        message:
          'Invalid DOM Nesting: p elements cannot have div, h1, h2, h3, h4, h5, h6, hr, ol, p, table or ul elements as children',
      },

      // Warn on nesting any invalid elements inside of <table> elements
      {
        selector:
          "JSXElement[openingElement.name.name='table'] > JSXElement[openingElement.name.name!=/^(caption|colgroup|tbody|tfoot|thead)$/][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: table elements cannot have element which are not caption, colgroup, tbody, tfoot or thead elements as children',
      },

      // Warn on nesting any invalid elements inside of <tbody>, <thead> and <tfoot> elements
      {
        selector:
          "JSXElement[openingElement.name.name=/(tbody|thead|tfoot)/] > JSXElement[openingElement.name.name!='tr'][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: tbody, thead and tfoot elements cannot have non-tr elements as children',
      },

      // Warn on nesting any invalid elements inside of <tr> elements
      {
        selector:
          "JSXElement[openingElement.name.name='tr'] > JSXElement[openingElement.name.name!=/(th|td)/][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: tr elements cannot have elements which are not th or td elements as children',
      },
  },
};

module.exports = config;

This handles the things we see that students most commonly need:

  1. Enforcing nesting rules for:
    • table, thead, tbody, tfoot, tr, th, td
    • ol, ul, li
  2. Warning against common nesting errors with:
    • p and common elements which are not allowed
    • nesting a elements, button elements and framework Link components inside of each other

Looks like this on some invalid code:

Screenshot 2023-03-08 at 19 22 22 Screenshot 2023-03-08 at 19 21 59

Is a lint or similar that can identify this issue? As others have pointed out, it’s quite difficult without better tooling.

@maxcountryman in the meantime, before the error message is improved, there are some tooling things here:

  1. I proposed this be a rule for eslint-plugin-react here: https://github.com/jsx-eslint/eslint-plugin-react/issues/3310
  2. seems there is already an ESLint plugin from @MananTank that does this https://github.com/MananTank/eslint-plugin-validate-jsx-nesting

Is a lint or similar that can identify this issue? As others have pointed out, it’s quite difficult without better tooling.

Another update on a lint rule, cross-posting my comment from jsx-eslint/eslint-plugin-react#3310 (comment):

Using some of the original validateDOMNesting code from @sophiebits and the src/mapping.js file from @MananTank’s validate-html-nesting, I was able to come up with a simple set of rules to prevent most footguns for our students using ESLint’s no-restricted-syntax rule (uses @typescript-eslint/utils for the config type at the first line):

(This has also been released as part of @upleveled/eslint-config-upleveled@3.13.0)

.eslintrc.cjs

/** @type {import('@typescript-eslint/utils').TSESLint.Linter.Config} */
const config = {
  rules: {
    'no-restricted-syntax': [
      'warn',
      // Warn on nesting <a> elements, <button> elements and framework <Link> components inside of each other
      {
        selector:
          "JSXElement[openingElement.name.name='a'] > JSXElement[openingElement.name.name=/^(a|button|Link)$/]",
        message:
          'Invalid DOM Nesting: anchor elements cannot have anchor elements, button elements or Link components as children',
      },
      {
        selector:
          "JSXElement[openingElement.name.name='button'] > JSXElement[openingElement.name.name=/^(a|button|Link)$/]",
        message:
          'Invalid DOM Nesting: button elements cannot have anchor elements, button elements or Link components as children',
      },
      {
        selector:
          "JSXElement[openingElement.name.name='Link'] > JSXElement[openingElement.name.name=/^(a|button|Link)$/]",
        message:
          'Invalid DOM Nesting: Link components cannot have anchor elements, button elements or Link components as children',
      },

      // Warn on nesting of non-<li> elements inside of <ol> and <ul> elements
      {
        selector:
          "JSXElement[openingElement.name.name=/^(ol|ul)$/] > JSXElement[openingElement.name.name!='li'][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: ol and ul elements cannot have non-li elements as children',
      },

      // Warn on nesting common invalid elements inside of <p> elements
      {
        selector:
          "JSXElement[openingElement.name.name='p'] > JSXElement[openingElement.name.name=/^(div|h1|h2|h3|h4|h5|h6|hr|ol|p|table|ul)$/]",
        message:
          'Invalid DOM Nesting: p elements cannot have div, h1, h2, h3, h4, h5, h6, hr, ol, p, table or ul elements as children',
      },

      // Warn on nesting any invalid elements inside of <table> elements
      {
        selector:
          "JSXElement[openingElement.name.name='table'] > JSXElement[openingElement.name.name!=/^(caption|colgroup|tbody|tfoot|thead)$/][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: table elements cannot have element which are not caption, colgroup, tbody, tfoot or thead elements as children',
      },

      // Warn on nesting any invalid elements inside of <tbody>, <thead> and <tfoot> elements
      {
        selector:
          "JSXElement[openingElement.name.name=/(tbody|thead|tfoot)/] > JSXElement[openingElement.name.name!='tr'][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: tbody, thead and tfoot elements cannot have non-tr elements as children',
      },

      // Warn on nesting any invalid elements inside of <tr> elements
      {
        selector:
          "JSXElement[openingElement.name.name='tr'] > JSXElement[openingElement.name.name!=/(th|td)/][openingElement.name.name!=/^[A-Z]/]",
        message:
          'Invalid DOM Nesting: tr elements cannot have elements which are not th or td elements as children',
      },
  },
};

module.exports = config;

This handles the things we see that students most commonly need:

  1. Enforcing nesting rules for:

    • table, thead, tbody, tfoot, tr, th, td
    • ol, ul, li
  2. Warning against common nesting errors with:

    • p and common elements which are not allowed
    • nesting a elements, button elements and framework Link components inside of each other

Looks like this on some invalid code:

Screenshot 2023-03-08 at 19 22 22 Screenshot 2023-03-08 at 19 21 59

This is going to help a ton. There are a couple of more rules that could be added, how can I contribute?