Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: validateDOMNesting Hydration failed #24519

Closed
icyJoseph opened this issue May 9, 2022 · 20 comments
Closed

Bug: validateDOMNesting Hydration failed #24519

icyJoseph opened this issue May 9, 2022 · 20 comments
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@icyJoseph
Copy link

icyJoseph commented May 9, 2022

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.

@icyJoseph icyJoseph added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label May 9, 2022
@icyJoseph icyJoseph changed the title Bug: validateDOMNesting error produces Hydration failed on SSR Bug: validateDOMNesting error produces wrong HTML nodes in SSR May 9, 2022
@nmain
Copy link

nmain commented May 10, 2022

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.

@icyJoseph
Copy link
Author

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.

@fabb
Copy link

fabb commented Sep 18, 2022

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.

@Nefcanto
Copy link

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.

@fabb
Copy link

fabb commented Nov 27, 2022

I think that is more a problem of React than Next.js, no?

@icyJoseph
Copy link
Author

When the error is triggered by invalid DOM nesting it is almost rather trivial to figure it out though. And in many cases it is printed to the console. Granted there are cases where its hard to tell but then again, but I can hardly remember any. A tool like can i include can help out to validate DOM tag nesting as well.

And yeah, Next.js is a React Framework for production, not React itself.

@shrutivtu
Copy link

I tried using tbody and thead...didnt work for me. Trying other methods now and making it from scratch.

@Undead34
Copy link

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

@nkilm
Copy link

nkilm commented Jan 5, 2023

Is there any way to locate where the invalid nesting is present?

@meotimdihia
Copy link

meotimdihia commented Jan 15, 2023

It is impossible to find where the error is.
I just have seen this error on Sentry.
But when I open the URL and check the console. I can't see the error.

Currently, we have to guess where it is. Sometimes it is super hard.

@DeclanMurphy
Copy link

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?

@maxcountryman
Copy link

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

@timneutkens
Copy link
Contributor

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

@karlhorky
Copy link
Contributor

karlhorky commented Feb 26, 2023

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.

@karlhorky
Copy link
Contributor

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: Validate DOM nesting when possible jsx-eslint/eslint-plugin-react#3310
  2. seems there is already an ESLint plugin from @MananTank that does this https://github.com/MananTank/eslint-plugin-validate-jsx-nesting

@meotimdihia
Copy link

it is impossible to fix this problem for me.
Sentry logged this error once per client and the error disappeared, I can't reproduce it.
And Sentry didn't log the Component Stack for this error.

@karlhorky
Copy link
Contributor

karlhorky commented Mar 8, 2023

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

@icyJoseph
Copy link
Author

icyJoseph commented Jun 1, 2023

After all of this time, being out there helping out folks, and plain just learning more myself, I've updated the OP, and I feel good enough to close this issue.

Do remember that this was opened mostly because of validateDOMNesting going from a error-warning to crashing out, as I originally stated, this was always present, and it was only when React 18 was release that it started to be something people paid attention to. All things considered perhaps that was good.

@icyJoseph icyJoseph changed the title Bug: validateDOMNesting error produces wrong HTML nodes in SSR Bug: validateDOMNesting Hydration failed Jun 1, 2023
@adeleke5140
Copy link

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?

@karlhorky
Copy link
Contributor

karlhorky commented Sep 23, 2023

Amazing, glad it can be helpful!

how can I contribute?

Three ways:

  1. Contribute to our simple version using no-restricted-syntax here: https://github.com/upleveled/eslint-config-upleveled/blob/8042b1ff73c1f7d322f80caf796257a4df86cb69/index.cjs#L66
  2. Check out the much more comprehensive eslint-plugin-validate-jsx-nesting by @MananTank
  3. If you would manage to contribute this functionality to eslint-plugin-react like you wrote about over here, that would be the most amazing option for the community, I think - it would help many people

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests