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.
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.
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.