@aiera-inc/react-slots
Version:
A simple utility for adding named slots to React components, inspired by the slot pattern in Vue.
207 lines (156 loc) • 5.91 kB
Markdown
# react-slots
A simple utility for adding named slots to React components, inspired by the slot pattern in web components and Vue.
## Features
- **Named slots**: Expand on React's children by defining specific child component options.
- **Structured layout**: Assign your slot components to static locations in the JSX.
- **Multiple/Repeatable slots**: Allow multiple children for a slot by wrapping the slot component in an array.
- **Composed components**: Add components to the namespace without rendering them as slots.
- **TypeScript support**: Strongly typed slot schemas and props.
- **Tiny bundle size**: A whopping 1.1kb file.
## Installation
Add the package to your React application using your preferred package manager.
```
npm i aiera-inc/react-slots
```
Alternatively copy the `react-slots.tsx` file directly into your project. It's that small!
## Usage
Slotted components are created using the `SlottedComponent` utility method exported by the library and consumed just like any other React component.
```tsx
import { SlottedComponent } from 'react-slots';
// Your parent components props
type MyComponentProps = {
author: string;
};
// The components you are assigning as slots
const SLOT_SCHEMA = {
Title: (props: { title: string }) => <header>{props.title}</header>,
Footer: () => <footer>Footer</footer>,
} as const;
// Your slotted component
export const MyComponent = SlottedComponent(SLOT_SCHEMA)<MyComponentProps>(({
author,
children,
slots: { Title, Footer },
}) => {
return (
<div>
{Title}
<h3>By: {author}</h3>
<main>{children}</main>
{Footer}
</div>
);
});
```
### Using Your Component
Your slotted component works like any other React component, only it now has several slots added as properties to it.
```tsx
import { MyComponent } from './my-component';
export function App() {
return (
<div>
<MyComponent author="John Doe">
<MyComponent.Title title="Hello World" />
<article>This is the main content.</article>
<article>A second article.</article>
<MyComponent.Footer />
</MyComponent>
</div>
);
}
```
Note, the order of the slotted child components doesn't actually matter because the `MyComponent` code has placed it in a concrete location within it's layout.
```jsx
function App() {
return (
<>
// This is equivalent...
<MyComponent author="John Doe">
<MyComponent.Title title="Hello World" />
<article>This is the main content.</article>
<article>A second article.</article>
<MyComponent.Footer />
</MyComponent>
// ...to this
<MyComponent author="John Doe">
<MyComponent.Title title="Hello World" />
<MyComponent.Footer />
<article>This is the main content.</article>
<article>A second article.</article>
</MyComponent>
</>
);
}
```
## Features
The _slot schema_ has two unique syntax structures that can be used to define **repeated slots** and **namespace slots**.
### Repeated Slots
By default, only one instance of a component will be passed to a slot and the others will be tossed.
```tsx
const ArticleExample = (props) => <article>{props.children}</article>;
const SLOT_SCHEMA = {
Article: ArticleExample,
} as const;
const Blog = SlottedComponent(SLOT_SCHEMA)<{ author: string }>(({
author,
children,
slots: { Article },
}) => {
return <div>{Article}</div>;
});
function App() {
return (
<Blog author="John Doe">
<Blog.Article>Article 1</Blog.Article>
<Blog.Article>Article 2</Blog.Article> // This will be ignored
</Blog>
);
}
```
To allow for repeated uses of a slot, you simply need to wrap it in square brackets in the slot schema. This tells the utility that multiple instances of the `<Article />` slot may be passed and should be rendered.
```tsx
const ArticleExample = (props) => <article>{props.children}</article>;
const SLOT_SCHEMA = {
Article: [ArticleExample],
} as const;
```
### Namespaced Slots (Composed Components)
Sometimes you may want to link components to indicate a relationship between parent and child, but you don't have a concrete location that you want to place the children in your layout. You are effectively namespacing the child component to the parent. This is often also described as **composed components**.
```tsx
const ArticleExample = (props) => <article>{props.children}</article>;
const SLOT_SCHEMA = { // Wrapping the component in curly braces
Article: { ArticleExample }, // tells the utility this is a namespaced
} as const; // component.
const Blog = SlottedComponent(SLOT_SCHEMA)<{ author: string }>(({
author,
children, // children contains your Article instances
slots, // Slots will be an empty object
}) => {
return <div>{children}</div>;
});
function App() {
return (
<Blog author="John Doe">
<Blog.Article>Article 1</Blog.Article>
<Blog.Article>Article 2</Blog.Article>
</Blog>
);
}
```
## API Reference
### `withSlots(Parent, schema)`
Wraps your component and attaches slot components as static properties. The `slots` prop will be injected into your component, containing the slotted children.
### `getSlots(children, schema)`
Utility to extract slots and non-slot children from a component's children.
### `SlottedComponent(schema)`
Higher-order component for applying a slot schema in a more declarative way.
## TypeScript Types
- `SlotDictionary`: Defines the slot schema.
- `WithSlotProps`: Utility type for merging slot props into your component props.
- `SlotProviderInterface`: Interface alternative for slot props.
## Notes
- Nested child fragments of composed components will be automatically flattened.
- Nesting fragments by 2 or more levels is not supported.
- Namespaced slots are not included in the `slots` prop.
## License
MIT