DEV Community

Cover image for Does React Break HTML Rules? Let's Find Out
Ayman Eldawy
Ayman Eldawy

Posted on

Does React Break HTML Rules? Let's Find Out

Can React create HTML that the browser itself wouldn't allow?

Sounds wrong, right?

That's what I thought too.

But while experimenting with React, I noticed something weird. So instead of guessing what's happening, let's break some HTML and see where it takes us.

And before looking at the results, try to guess what will happen yourself.

Let's Break Some HTML

Take this:

<p>
  Hello
  <div>World</div>
</p>
Enter fullscreen mode Exit fullscreen mode

Nothing complicated.

Except it's invalid HTML.

A <p> element can only contain phrasing content, so a <div> can't live inside it.

Here's the first question:

What do you think the DOM will look like?

You might expect something like this:

p
├── "Hello"
└── div
    └── "World"
Enter fullscreen mode Exit fullscreen mode

But open DevTools and inspect it.

You'll get something closer to:

<p>Hello</p>
<div>World</div>
<p></p>
Enter fullscreen mode Exit fullscreen mode

The <div> is no longer inside the paragraph.

What happened?

When the HTML parser encounters the <div>, its parsing rules close the open <p> first. Later, when it reaches our original closing </p>, it has to deal with that too, which is why we can end up with that strange empty paragraph.

Okay.

We gave the browser invalid HTML, and the parser repaired it.

Fair enough.

Now let's annoy React with the same thing.

function App() {
  return (
    <p>
      Hello
      <div>World</div>
    </p>
  );
}
Enter fullscreen mode Exit fullscreen mode

React warns us about the invalid nesting.

But here's where I originally got confused.

During client rendering, the DOM can still end up with that structure: the <div> inside the <p>.

Wait.

Didn't the browser just "fix" the exact same structure?

Why did one get repaired while the other didn't?

My first explanation was:

React handles HTML differently from the browser.

Not a terrible guess.

Also not quite right.

Maybe React Isn't the Interesting Part

Let's remove React completely.

Try this:

const p = document.createElement("p");
const div = document.createElement("div");

p.appendChild(div);
document.body.appendChild(p);
Enter fullscreen mode Exit fullscreen mode

Inspect the DOM again.

We just constructed a <div> inside a <p> using plain JavaScript.

So...

React wasn't the important part after all.

The real difference is how we reached the DOM.

When the browser receives:

<p>
  <div></div>
</p>
Enter fullscreen mode Exit fullscreen mode

it has HTML text that needs to be parsed.

Roughly:

HTML
  ↓
HTML parser
  ↓
DOM
Enter fullscreen mode Exit fullscreen mode

The HTML parser has a surprisingly large set of rules for turning that text into a DOM tree, including rules for recovering from invalid markup.

But document.createElement() takes another path.

We're explicitly creating nodes and connecting them ourselves:

JavaScript
  ↓
DOM APIs
  ↓
DOM
Enter fullscreen mode Exit fullscreen mode

There's no <p><div></div></p> HTML string going through the HTML parser in that example.

That's the part I was missing.

And it gives us a better way to think about what we saw with React.

JSX may look a lot like HTML, but during normal client rendering React isn't simply handing that JSX to the browser as an HTML document and asking the HTML parser to process it.

So two pieces of code can look almost identical while taking different paths to the DOM.

Okay, Let's Make It Weirder

Once I understood that, obviously I had to try more broken HTML.

Take this:

<image src="cat.png">
Enter fullscreen mode Exit fullscreen mode

Quick guess.

What element do you think you'll find in the DOM?

<image>?

Nope.

The HTML parser has a wonderfully specific rule for this.

When it encounters an image start tag, it treats it as a parse error, changes the tag name to img, and processes it again.

So we end up with:

<img src="cat.png">
Enter fullscreen mode Exit fullscreen mode

Yes, this behavior is actually in the HTML specification.

Now compare that with:

const element = document.createElement("image");
document.body.appendChild(element);
Enter fullscreen mode Exit fullscreen mode

createElement() creates the element we requested. Because image isn't a recognized HTML element name, browsers represent it as an HTMLUnknownElement.

Again, we're not asking the HTML parser to interpret an <image> token.

We're constructing an element directly.

And suddenly what looked like:

"React is ignoring HTML rules."

looks more like:

"I was comparing HTML parsing with programmatic DOM construction."

Much better question.

Then SSR Enters the Room

At this point you could say:

"Cool. Weird browser trivia. I'll forget it tomorrow."

Fair.

But there's one place where this becomes very practical for React developers:

Server-side rendering.

With SSR, React produces HTML on the server.

That HTML reaches the browser.

Which means...

Yep.

We're back to the HTML parser.

React on the server
      ↓
     HTML
      ↓
 HTML parser
      ↓
     DOM
      ↓
React hydration
Enter fullscreen mode Exit fullscreen mode

Now imagine the server sends invalid nesting.

Before hydration starts, the browser parses that HTML. If the parser repairs the structure, React can find a DOM tree that doesn't match what it expected to hydrate.

Hello, hydration mismatch.

React itself lists invalid HTML tag nesting as one possible cause of hydration failures.

So those invalid nesting warnings aren't React being dramatic for no reason.

Something that seems harmless while you're experimenting with client rendering can become a real problem once server-rendered HTML and hydration enter the picture.

So, Does React Break HTML Rules?

Not really.

My original question was slightly wrong.

I was treating "the browser" as if it were one single mechanism.

It isn't.

HTML text goes through the HTML parser, which has rules for building a DOM tree and recovering from invalid markup.

JavaScript can construct DOM nodes programmatically without sending the same HTML text through that parser.

And React's client rendering doesn't follow the same path as parsing a server-delivered HTML document.

With SSR, however, HTML parsing comes back into the picture.

So the next time the browser mysteriously "fixes" your HTML or React starts complaining about hydration, there's a useful question to ask:

Who built this DOM: the HTML parser, or JavaScript?

Top comments (0)