Skip to content

Flammae/react-slots

Repository files navigation

beqa/react-slots - Responsible React Parenting

react-slots empowers you to prioritize composability in your component APIs.

Featuring

  • Lightweight (< 8KB minified, < 3KB minified & gzipped)
  • Composability with ease
  • Type-safety
  • Server Components support
  • Not implemented with context
  • Intuitive API
  • Self-documenting with typescript
  • Elegant solution to a11y attributes
  • Inversion of control

Installation

The installation process consists of two parts: installing the core library (around 3KB gzipped piece of code that runs in your users' browsers and handles the core logic) and an optional compile-time plugin (for transpiling JSX syntax for your slot elements into regular function invocations).

Installation steps

Docs

You can find the docs on the docs website

Discord

If you need any assistance, feel free to join our Discord server

Implementing

import { useSlot, SlotChildren, Slot } from "@beqa/react-slots";

type ListItemProps = {
  children: SlotChildren<
    | Slot<"title"> // Shorthand of Slot<"title", {}>
    | Slot<"thumbnail"> // Shorthand of Slot<"thumbnail", {}>
    | Slot<{ isExpanded: boolean }> // Shorthand of Slot<"default", {isExpanded: boolean}>
  >;
};

function ListItem({ children }: ListItemProps) {
  const { slot } = useSlot(children);
  const [isExpanded, setIsExpanded] = useState();

  return (
    <li
      className={`${isExpanded ? "expanded" : "collapsed"}`}
      onClick={() => setIsExpanded(!isExpanded)}
    >
      {/* Render thumbnail if provided, otherwise nothing*/}
      <slot.thumbnail />
      <div>
        {/* Render a fallback if title is not provided*/}
        <slot.title>Expand for more</slot.title>
        {/* Render the description and pass the prop up to the parent */}
        <slot.default isExpanded={isExpanded} />
      </div>
    </li>
  );
}

Specifying Slot Content From the Parent

With slot-name attribute

<ListItem>
  <img slot-name="thumbnail" src="..." />
  <div slot-name="title">A title</div>
  this is a description
</ListItem>

With Templates

import { template } from "beqa/react-slots";

<ListItem>
  <template.thumbnail>
    <img src=".." />
  </template.thumbnail>
  <template.title>A title</template.title>
  <template.default>
    {({ isExpanded }) =>
      isExpanded ? <strong>A description</strong> : "A description"
    }
  </template.default>
  <template.default>doesn't have to be a function</template.default>
</ListItem>;

With type-safe templates

// Option #1
import { createTemplate } from "@beqa/react-slots";
const template = createTemplate<ListItemProps["children"]>();

// Option #2
import { template, CreateTemplate } from "@beqa/react-slots";
const template = template as CreateTemplate<ListItemProps["children"]>;

// Typo-free and auto-complete for props!
<ListItem>
  <template.thumbnail>
    <img src="..." />
  </template.thumbnail>
  <template.title>A title</template.title>
  <template.default>
    {({ isExpanded }) =>
      isExpanded ? <strong>A description</strong> : "A description"
    }
  </template.default>
  <template.default>doesn't have to be a function</template.default>
</ListItem>;

Advanced Examples

The code samples below represent actual implementations. No need to define external state or event handlers for these components to function.

Creating highly composable Accordion and AccordionList components using react-slots

Checkout live example

<AccordionList>
  <Accordion key={1}>
    <span slot-name="summary">First Accordion</span>
    This part of Accordion is hidden
  </Accordion>
  <Accordion key={2}>
    <span slot-name="summary">Second Accordion</span>
    AccordionList makes it so that only one Accordion is open at a time
  </Accordion>
  <Accordion key={3}>
    <span slot-name="summary">Third Accordion</span>
    No external state required
  </Accordion>
</AccordionList>

Creating highly composable Dialog and DialogTrigger components using react-slots

Checkout live example

<DialogTrigger>
  <Button>Trigger Dialog</Button>
  <Dialog slot-name="dialog">
    <span slot-name="title">Look Ma, No External State</span>
    <p slot-name="content">... And no event handlers.</p>
    <p slot-name="content">Closes automatically on button click.</p>
    <p slot-name="content">Can work with external state if desired.</p>
    <Button
      slot-name="secondary"
      onClick={() => alert("But how are the button variants different?")}
    >
      Close??
    </Button>
    <Button slot-name="primary">Close!</Button>
  </Dialog>
</DialogTrigger>

If you like this project please show support by starring it on Github