Jul 17, 2023

A simple slot system for React

Article splash image

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 the TestContainer
  • 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 the children 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.