Skip to content

Instantly share code, notes, and snippets.

@giuseppelt
Last active April 29, 2024 13:17
Show Gist options
  • Save giuseppelt/14d2bda071f728f5164baa76ecf60b01 to your computer and use it in GitHub Desktop.
Save giuseppelt/14d2bda071f728f5164baa76ecf60b01 to your computer and use it in GitHub Desktop.
Simple React slot system

The complete tutorial for this slot system for react
https://www.breakp.dev/blog/simple-slot-system-for-react/

With this tiny Slot component, you can use slots inside React components.

In addition it supports

  • named slots
  • default slot
  • required slot
  • optional fallback element, if the slot is not provided

The Slot component is very small and does all the work.

Example usage

Parent component

import { Slot } from "./components"; 

export function TestContainer({ children } ) {
  return (
    <div>
      <Slot name="header" required children={children} />
      <p>content</p>
      <Slot children={children} />
      <Slot name="footer" fallback={<p>Default Footer</p>} children={children} />
    </div>
  );
}

The test container:

  • requests a "header" slot, required, will error if not provided
  • includes the default slot, that is all children non-slotted
  • requests a "footer" slot, optional, with fallback content if not provided

Use the component

import { TestContainer } from "./components"; 

export function ComplexComponent() {
   return (
      <TextContainer>
        <p slot="header">This will be the header</p>
        
        <p>Other body content</p>
        <p>Other body content</p>
        <p>Other body content</p>
      </TextContainer>
   );
}
import { ReactElement, ReactNode, cloneElement, isValidElement } from "react";
// Add the slot property to all React component
// you can leave it here
// or move it in your env.d.ts
declare global {
namespace React {
interface Attributes {
slot?: string
}
}
}
export type SlotProps = {
name?: string
required?: boolean
fallback?: ReactElement
children?: ReactNode
}
export function Slot(props: SlotProps) {
const {
name,
children,
required,
fallback,
} = props;
// this is a default slot, that is, non-slotted children
if (!name) {
return getDefaultSlot(children);
}
// otherwise get it
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;
}
function getSlot(children: ReactNode, name: string): ReactElement | undefined {
if (children) {
if (Array.isArray(children)) {
return children.find(x => isValidElement(x) && (x.props as any)?.slot === name);
} else if (isValidElement(children) && children.props?.slot === name) {
return children;
}
}
}
function getDefaultSlot(children: ReactNode) {
if (children) {
if (isValidElement(children)) {
return children.props?.slot ? null : children;
} else if (Array.isArray(children)) {
return children.map(x => x && isValidElement(x) && (x.props as any)?.slot ? null : x);
}
}
return children;
}
@tsanyqudsi
Copy link

if you want to avoid using any in isValidElement(x) && (x.props as any)? you can use isValidElement<{slot: string | undefined}>(x) && x.props.slot instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment