A simple slot system for React
This article propose a simple Slot component for React to create flexible and reusable templated components with customizable parts
Introduction
React doesnโt provide a slot system out of the box. Usually, the recommended way to provide parts to a component is to pass them as properties.
HTML web components and other frameworks offer slots as way to create flexible components. Slots are useful to create reusable templated components, where a host component can define slots, which are parts of the component that can be customized.
As example, a component that define a title
and a footer
slot.
<template id="article-component">
<header>
<h1><slot name="title">This is the Title</slot></h1>
</header>
<div>
This is the content of the component
<slot name="footer"></slot>
</div>
</template>
It can be reused in any page:
<article-component>
<span slot="title">My article title</span>
<span slot="footer">Subscribe for more!!</span>
</article-component>
The slots provided will fill the relative placeholders defined in the template.
The Slot
Component
With a simple custom component, the same can be achieved in React.
Hereโs the proposed Slot
component:
export function Slot({ name, required, fallback, children }) {
// no name is the default slot, that is, non-slotted children
if (!name) {
return getDefaultSlot(children);
}
// otherwise get it by name
let Content = getSlot(children, name);
if (Content) {
// remove slot property
return cloneElement(Content, { slot: undefined });
}
if (!Content && required) {
throw new Error(`Slot(${name}) is required`);
}
return Content ?? fallback ?? null;
}
Example usage
Named and default slots
import { Slot } from "./components";
export function TestContainer({ children } ) {
return (
<div>
<Slot name="header" children={children} />
<p>Builtin content</p>
<Slot children={children} />
</div>
);
}
The TestContainer
:
- defines an optional โheaderโ slot
- includes the default slot โ the unnamed slot โ where all natural children will be placed
To use the component:
import { TestContainer } from "./components";
export function Component() {
return (
<TextContainer>
<h1 slot="header">This will be the header</h1>
<p>Body content 1</p>
<p>Body content 2</p>
<p>Body content 3</p>
</TextContainer>
);
}
In this example, inside the Component
:
- the
<h1>
, will fill the โheaderโ slot of theTestContainer
- all other children (all the
<p>
s) will be placed in the default slot
Required slot
A component can flag a slot as required.
import { Slot } from "./components";
export function TestContainer({ children } ) {
return (
<div>
<p>Builtin Title</p>
<Slot name="content" required children={children} />
</div>
);
}
If the โcontentโ slot is not provided, an error will crash the application.
Fallback element
For optional slots, that is, non-required slots, a fallback component can be set. The slot will show it, if nothing is provided.
import { Slot } from "./components";
export function TestContainer({ children } ) {
return (
<div>
<p>Builtin Content</p>
<Slot name="footer" children={children} fallback={
<footer>
<p>This is the default footer</p>
</footer>
} />
</div>
);
}
If the slot โfooterโ is not provided, than the fallback <footer>
is shown instead.
Features
The Slot
components supports:
- Named slots โ part of the component identified by a string name
- Default slot โ a unique unnamed slot, where the natural children of the component will be placed
- Required slot โ the slot must be provided, otherwise en error is thrown
- Fallback content โ the base content that will be used, if the slot is not provided
In addition, any component can be passed in for the slot, not just html tags, but any React component can be used as slot, both your own components or the ones provided by any library.
Caveats
- The
Slot
component requires thechildren
of the parent component to be passed in. A workaround can be made to avoid this, but it requires a more complex setup with context. For the sake of simplicity, weโll leave it as it is. - Thereโs no checking on the slot name โ unmatched and misspelled slots will not fill.
- Slots can be repeated โ if a slot is repeated in the host component, the provided elements will be duplicated; if a named slot is provided multiple times, only the first one will be used.
Make typescript happy
The slot
property is not defined for every component. In order to not break type checking in typescript, a little trick must be done.
We can add the slot
property to any component thanks to declaration merging.
declare global {
namespace React {
interface Attributes {
slot?: string
}
}
}
You can either paste the code
- above the
Slot
component - or in the
env.d.ts
if your project has one
Remarks
A slot system is useful but itโs not always the best choice. React encourages use of explicit props, with a component or a render function callback.
For someone this feels unnatural and prefers a more stylish markup-oriented mechanism.
But, in the end, is all about tradeoffs. And, all things that taste good, bring some harm along with them.