Skip to content

Build1 publisher3 min readPublished

Two paths to the DOM: why invalid nesting shows up as a hydration mismatch

The HTML parser repairs a div inside a p. DOM APIs do not. That asymmetry lets one React component produce two different trees, depending on where it rendered.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Two paths to the DOM: why invalid nesting shows up as a hydration mismatch
Generated illustration

What happened

  • A p element can only contain phrasing content, so a div cannot be nested inside it.
  • Given the markup <p>Hello<div>World</div></p>, inspecting the DOM yields something closer to <p>Hello</p><div>World</div><p></p>, with the div no longer inside the paragraph.
  • When the HTML parser encounters the div, its parsing rules close the open p first; when it later reaches the original closing </p> it has to deal with that too, which is how an empty paragraph can appear.
  • Writing the same p/div structure as JSX makes React warn about the invalid nesting, but during client rendering the DOM can still end up with the div inside the p.
  • With React removed, document.createElement("p"), document.createElement("div"), p.appendChild(div) and document.body.appendChild(p) constructs a div inside a p in the DOM using plain JavaScript.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A walkthrough published on dev.to under the title "Does React Break HTML Rules? Let's Find Out" traces a familiar-looking React bug back to something that is not React's doing: invalid nesting is repaired when markup travels through the browser's HTML parser, and is not repaired when the same tree is assembled through DOM APIs [12][6]. The consequence for anyone shipping server-side rendering is that identical component code can legitimately yield two different DOM trees, and the mismatch is structural rather than cosmetic [13].

Start with the canonical offender. A `p` element may contain only phrasing content, so a `div` inside it is invalid [1]. Feed the browser `<p>Hello<div>World</div></p>` as markup and the resulting DOM is closer to `<p>Hello</p><div>World</div><p></p>` [2]. The parser's rules close the open `p` when the `div` start tag arrives, and the original closing `</p>` then accounts for the stray empty paragraph at the end [3].

Write the same thing as JSX and React warns about the invalid nesting, but during client rendering the DOM can still end up with the `div` sitting inside the `p` [4]. The tempting conclusion is that React treats HTML differently from the browser. The author's test removes React entirely: `document.createElement("p")`, `document.createElement("div")`, `appendChild`, and the div is inside the p again [5]. React was not the variable. The variable is the route to the DOM: an HTML string is parsed by a component with a large rule set, including rules for recovering from invalid markup, while DOM API calls construct and connect nodes directly with no parse step in between [6]. JSX resembles HTML, but in normal client rendering React is not handing an HTML document to the parser for interpretation [7].

The second example is sharper because it is specified behaviour. Given an `image` start tag in markup, the parser treats it as a parse error, renames the tag to `img`, and reprocesses it, so `<image src="cat.png">` becomes `<img src="cat.png">` [8]. Call `document.createElement("image")` and you get exactly the element you asked for; because `image` is not a recognised HTML element name, browsers represent it as an `HTMLUnknownElement` [9].

Server-side rendering puts the parser back in the pipeline. React produces HTML on the server, that HTML reaches the browser, the parser turns it into a DOM, and only then does hydration run [10]. If the server-rendered HTML contains invalid nesting and the parser repairs it, React can arrive to hydrate a tree that is not the tree it expected [11].

The practical reading is a testing gap. A component that renders block content inside a paragraph behaves consistently in a client-only render, because nothing repairs it there, and diverges only once the same markup passes through the parser on the SSR path [13]. Nesting warnings in development are therefore worth treating as correctness failures rather than lint noise, particularly in shared layout components and third-party wrappers that decide what tag encloses your children. The source is a single author's write-up, and it stops short of cataloguing React's own guidance on the subject, so the invariant is the useful takeaway: parser output and programmatic construction are not the same tree.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories