bp

Another way React Breaks HTML

shocked, sad, scared react logo

React users who are unaware of the limitations of react and JSX may be confused when putting react components inside web component (i.e. slotting them in). Despite their superficial similarities and the popular perception, react components are not HTML elements and JSX isn't HTML. Because of that, React components which fail to forward their props to their containing element (specifically the slot prop, which in most cases react should coerce to the slot attribute) will not project into the correct native slot. Those elements will project into the default slot if it exists, and disappear if it does not.

What are Native Slots?

Skip this section

Before we elaborate on the problem and introduce the workarounds, it's worth explaining what <slot> does. The web browser you are reading this post with comes pre-compiled with it's own built-in component model, called web components. Web components are custom HTML elements. The primary way for those components to define their boundaries is with the Shadow DOM, which is a private, visible, parallel DOM structure attached to the element. When creating web components, we can write <slot> elements into their shadow DOM which instruct the browser where to project the custom element's children. Those projected children continue to exist in the normal document (often called the "light DOM", in distinction to the shadow DOM), but they appear visually in the place where their assigning slot element is in the shadow DOM, and can be styled by shadow CSS.

<pf-card> <p>...</p> <h2 slot=header>...</h2> <button slot=footer>...</button> </pf-card>
Diagram showing how slotted content projects into the shadow DOM. A paragraph (in green), without a slot attribute, projects into the default slot in the center of the card. An h2, in red and with the slot=header attribute, projects into the red header area. A button, in blue and with the slot=footer attribute, slots into the blue footer region.

Demonstration

Let's write a little demo to illustrate the problem. This demo will use a custom element called <pf-card> from the @patternfly/elements package, which has three slots - header for headings and such, the default slot for body content, and footer for action buttons and the like. To clarify the issue, we'll set red, green, and blue background colours on our card's slot regions, respectfully.

#card::part(header) { background-color: #f008; }
#card::part(body)   { background-color: #0f08; }
#card::part(footer) { background-color: #00f8; }

Now let's write two react components, one called ForwardsSlot which forwards its slot prop, and one called NoForwardsSlot which does not. We'll have both of these components print out which slot they are meant to be projected into. Then, we'll write an "App" component which sets our examples as children of a card, in each of the card's three slots and sandwich them with DOM nodes, so that we'll be able to tell right away which react components are rendering out of place.

import { Card } from "@patternfly/elements/react/pf-card/pf-card.js";

const printSlots = ({ slot }) => slot
  ? (<code>slot="{slot}"</code>)
  : (<span>Default slot</span>);

function ForwardsSlot(props) {
  return (
    <div slot={props?.slot}>
      <p>{printSlots(props)} <strong>forwarded</strong>.</p>
    </div>
  );
}

function NoForwardsSlot(props) {
  return (
    <div>
      <p>{printSlots(props)} <strong>not</strong> forwarded.</p>
    </div>
  );
}

export default function App() {
  return (
    <Card id="card" className="App" rounded>
      <small slot="header">Header content starts</small>
      <ForwardsSlot slot="header" />
      <NoForwardsSlot slot="header" />
      <small slot="header">Header content ends</small>

      <small>Body content starts</small>
      <ForwardsSlot />
      <NoForwardsSlot />
      <small>Body content ends</small>

      <small slot="footer">Footer content starts</small>
      <ForwardsSlot slot="footer" />
      <NoForwardsSlot slot="footer" />
      <small slot="footer">Footer content ends</small>
    </Card>
  );
}

According to the source props, we expect to see our components' content projected into the slots specified with the slot attribute, two in the head, two in the body, and two in the footer. If this was HTML, it would Just Work. But this is not HTML, iTs jUSt jAVasCriPt, so it should cOmPOse beTteR, bruh. Will we see our content in order?

Not so much. Instead, the subtle bug in react's virtual DOM abstraction has our non-forwarding components rendering into the default slot of the card. If there was no default slot, they would disappear.

Workarounds

So what can web developers stuck in react codebases do? In cases where the user is able to control the slotted react component, they should be sure to forward the slot prop to the container. Otherwise, they must wrap the react component in a DOM node and place the slot attribute on the wrapper.

function App() {
  return (
    <Card id="card" className="App" rounded>
      <div slot="header" style="display:contents;">
        <NoForwardsSlot />
      </div>
    </Card>
  );
}

Why Keep Digging?

It's often said how react improves the developer experience, but this right here is just another example of how React breaks web development, the web platform, and ultimately web developers' understanding of the medium in which they work.

collinsworth Josh Collinsworth @ hachyderm.io ✏️ 3 🔁 28 ❤️ 42

Psst! Hey you! Yeah you, the frontend dev with all the JSX files! ...Wanna know a secret?

You know all those articles you read with titles like "what's the difference between useMemo and useCallback, and when would you not want to use them?"

Or maybe, "How not to shoot yourself in the foot with useEffect?"

The secret is: other frameworks don't have nearly so many of those. They just work. You don't have to micromanage them.

React is way more antiquated than you think it is.

If you're still using React, and wondering why web development is so complicated, take Josh' words to heart.

Comments

I left Mastodon because of the overwheling antisemitism. The comments system on this blog was based on my now-defunct mastodon instance, so old comments on posts may disappear.

Send a webmention to any of my posts to comment, and hopefully it'll find its way through to tubes to this site.