Skip to content

Commit 76a4c64

Browse files
author
Viktor Pasynok
committed
docs: merge README_NEW.md content into README.md, enhancing documentation with slot examples, installation instructions, and community links
1 parent f196c7f commit 76a4c64

File tree

2 files changed

+173
-381
lines changed

2 files changed

+173
-381
lines changed

README.md

Lines changed: 173 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,251 @@
1-
# React Slots
1+
<p align="center">
2+
<img src="logo.webp" alt="React Slots" width="200" />
3+
</p>
24

3-
Bring the power of slots to your React components effortlessly.
5+
# React Slots
46

5-
## Table of Contents
7+
**Build extensible React components with slot-based architecture.** Define extension points where plugins and third-party code can inject content.
68

7-
- [Motivation](#motivation)
8-
- [How does this library solve this problem?](#how-does-this-library-solve-this-problem)
9-
- [Example](#example)
10-
- [Installation](#installation)
11-
- [How-to Guides](#how-to-guides)
12-
- [How to Pass Props to Components Inserted into a Slot](#how-to-pass-props-to-components-inserted-into-a-slot)
13-
- [How to Insert Multiple Components into a Slot](#how-to-insert-multiple-components-into-a-slot)
14-
- [How to Manage the Order of Components in a Slot](#how-to-manage-the-order-of-components-in-a-slot)
15-
- [Community](#community)
9+
## What are slots?
1610

17-
## Motivation
11+
**Slots** are named extension points in a component where content can be injected from outside.
1812

19-
In modern React applications, building reusable and **flexible** components is key to scaling efficiently. However, as the complexity of components increases, the need for a slot-based architecture becomes apparent. The concept of slots, familiar to developers from frameworks like Svelte and Vue, allows for seamless content injection and greater customization of component behavior. But in React, this pattern isn’t natively supported and often leads to verbose or suboptimal solutions.
13+
Vue example:
2014

21-
## How does this library solve this problem?
15+
```vue
16+
<!-- Sidebar.vue -->
17+
<aside>
18+
<nav>Core navigation</nav>
19+
<slot name="widgets"></slot>
20+
</aside>
2221
23-
`react-slots` introduces a streamlined way to implement slots, bringing familiar concepts into the React ecosystem with minimal effort. It provides developers with a clear and consistent API to define and use slots, enhancing flexibility while reducing boilerplate code. The library ensures components remain decoupled, making it easier to manage nested or complex content structures.
22+
<!-- Usage -->
23+
<Sidebar>
24+
<template #widgets>
25+
<AnalyticsWidget />
26+
<UserStatsWidget />
27+
</template>
28+
</Sidebar>
29+
```
2430

25-
## Example
31+
## The problem in React
2632

27-
This example demonstrates how to create and use slots in React using the `@grlt-hub/react-slots` library.
33+
React doesn't have a built-in slot system. This creates challenges when building **extensible architectures** where different parts of your app (or plugins) need to inject content into predefined locations.
2834

29-
### Code Breakdown
35+
### Example: Admin dashboard with plugins
3036

31-
1. **Creating Slot Identifiers**
37+
You're building an admin dashboard. Plugins should be able to add widgets to the sidebar without modifying the core `Sidebar` component:
3238

33-
```ts
34-
import { createSlots, createSlotIdentifier } from '@grlt-hub/react-slots';
39+
```tsx
40+
// Sidebar.tsx - core component (shouldn't change when plugins are added)
41+
export const Sidebar = () => (
42+
<aside>
43+
<nav>Core navigation</nav>
44+
{/* 🤔 How do plugins inject widgets here? */}
45+
</aside>
46+
);
3547

36-
const slots = {
37-
Bottom: createSlotIdentifier(),
38-
} as const;
48+
// plugin-analytics/index.ts - separate package
49+
// This plugin wants to add analytics widget to sidebar
50+
// How??? 🤷‍♂️
3951
```
4052

41-
We import `createSlots` and `createSlotIdentifier` from the library. Then, we define a slots object, where each key represents a unique slot. In this case, we create a slot named Bottom.
53+
### Standard approaches are awkward
4254

43-
2. **Creating the Slot API**
55+
- Collecting everything in parent component - tight coupling, parent must know all plugins
56+
- Context with manual management - lots of boilerplate per extension point
57+
- Passing render functions through props - verbose, non-intuitive API
4458

45-
```ts
46-
const { slotsApi: footerSlots, Slots: FooterSlots } = createSlots(slots);
47-
```
59+
## The solution
4860

49-
`createSlots` takes the `slots` object and returns two values:
61+
**With `@grlt-hub/react-slots`, define extension points once and inject components from anywhere:**
5062

51-
- `slotsApi` (renamed to `footerSlots`): an API for managing slot content.
52-
- `Slots` (renamed to `FooterSlots`): a component used to render the slot content in the specified location.
63+
```tsx
64+
// Sidebar.tsx - define the slot
65+
import { createSlots, createSlotIdentifier } from '@grlt-hub/react-slots';
5366

54-
3. **Defining the Footer Component**
67+
export const { slotsApi, Slots } = createSlots({
68+
Widgets: createSlotIdentifier(),
69+
} as const);
5570

56-
```tsx
57-
const Footer = () => (
58-
<footer>
59-
Hello
60-
<FooterSlots.Bottom />
61-
</footer>
71+
export const Sidebar = () => (
72+
<aside>
73+
<nav>Core navigation</nav>
74+
<Slots.Widgets /> {/* Extension point */}
75+
</aside>
6276
);
63-
```
6477

65-
Using `footerSlots.insert.into.Bottom`, we insert content into the `Bottom` slot. Here, we add a component that renders `<code>World</code>`.
78+
// plugin-analytics/index.ts - inject from anywhere!
79+
import { slotsApi } from './Sidebar';
6680

67-
### Result
81+
slotsApi.insert.into.Widgets({
82+
component: () => <AnalyticsWidget />,
83+
});
6884

69-
After executing the code, the rendered output will be:
85+
// plugin-user-stats/index.ts - another plugin
86+
import { slotsApi } from './Sidebar';
7087

71-
```html
72-
<footer>
73-
Hello
74-
<code>World</code>
75-
</footer>
88+
slotsApi.insert.into.Widgets({
89+
component: () => <UserStatsWidget />,
90+
});
7691
```
7792

78-
This way, the `@grlt-hub/react-slots` library provides an efficient way to define and use slots in React components, making content injection simple and flexible.
93+
### Result
94+
95+
```tsx
96+
<aside>
97+
<nav>Core navigation</nav>
98+
<AnalyticsWidget />
99+
<UserStatsWidget />
100+
</aside>
101+
```
102+
103+
No props drilling, no boilerplate - just define slots and inject content from anywhere in your codebase.
79104

80105
## Installation
81106

82107
```sh
83108
npm i @grlt-hub/react-slots
84109
# or
85-
yarn add @grlt-hub/react-slots
86-
# or
87110
pnpm add @grlt-hub/react-slots
111+
# or
112+
bun add @grlt-hub/react-slots
113+
# or
114+
yarn add @grlt-hub/react-slots
88115
```
89116

90-
## How-to Guides
117+
TypeScript types are included out of the box.
91118

92-
### How to Pass Props to Components Inserted into a Slot
119+
### Peer dependencies
93120

94-
In this guide, we'll walk through how to define and pass props to components inserted into a slot using `@grlt-hub/react-slots`.
121+
- `react` ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
122+
- `effector` 23
123+
- `effector-react` 23
124+
- `nanoid` \*
95125

96-
#### Step 1: Define a Slot with Props
126+
## Quick Start
97127

98-
You can specify the props a slot should accept by providing a type to `createSlotIdentifier`. For example, if you want a slot that requires a text prop, you can define it like this:
128+
Here's a minimal working example:
99129

100-
```ts
101-
import { createSlotIdentifier } from '@grlt-hub/react-slots';
130+
```tsx
131+
import { createSlots, createSlotIdentifier } from '@grlt-hub/react-slots';
102132

103-
const slots = {
104-
Bottom: createSlotIdentifier<{ text: string }>(),
105-
} as const;
106-
```
133+
// 1. Create slots
134+
const { slotsApi, Slots } = createSlots({
135+
Footer: createSlotIdentifier(),
136+
} as const);
137+
138+
// 2. Use slot in your component
139+
const App = () => (
140+
<div>
141+
<h1>My App</h1>
142+
<Slots.Footer />
143+
</div>
144+
);
107145

108-
This type definition ensures that any usage of `<FooterSlots.Bottom />` must include a `text` prop.
146+
// 3. Insert content into the slot
147+
slotsApi.insert.into.Footer({
148+
component: () => <p>© 1955–1985–2015 Outatime Corp.</p>,
149+
});
109150

110-
#### Step 2: Use the Slot in Your Component
151+
// Result:
152+
// <div>
153+
// <h1>My App</h1>
154+
// <p>© 1955–1985–2015 Outatime Corp.</p>
155+
// </div>
156+
```
157+
158+
## How-to Guides
111159

112-
When you use the slot component in your layout, you must pass the required props directly:
160+
### Pass props to inserted components
113161

114162
```tsx
115-
const Footer = () => (
116-
<footer>
117-
Footer content
118-
<FooterSlots.Bottom text='Hello from the slot!' />
119-
</footer>
120-
);
121-
```
163+
// Define slot with typed props
164+
const { slotsApi, Slots } = createSlots({
165+
UserPanel: createSlotIdentifier<{ userId: number }>(),
166+
} as const);
122167

123-
The `text` prop passed here will be provided to any component inserted into the `Bottom` slot.
168+
// Use in component
169+
<Slots.UserPanel userId={123} />;
124170

125-
#### Step 3: Insert a Component into the Slot
171+
// Insert component - receives props automatically
172+
slotsApi.insert.into.UserPanel({
173+
component: (props) => <UserWidget id={props.userId} />,
174+
});
175+
```
126176

127-
You use `footerSlots.insert.into.Bottom` to insert a component. The component will automatically receive the props passed to `<FooterSlots.Bottom />`:
177+
### Transform props with `mapProps`
128178

129179
```tsx
130-
footerSlots.insert.into.Bottom({
131-
fn: ({ text }) => ({ doubleText: `${text} ${text}` }),
132-
component: ({ doubleText }) => <p>{doubleText}</p>,
180+
const { slotsApi, Slots } = createSlots({
181+
UserPanel: createSlotIdentifier<{ userId: number }>(),
182+
} as const);
183+
184+
<Slots.UserPanel userId={123} />;
185+
186+
slotsApi.insert.into.UserPanel({
187+
// Transform userId into userName and isAdmin before passing to component
188+
mapProps: (slotProps) => ({
189+
userName: getUserName(slotProps.userId),
190+
isAdmin: checkAdmin(slotProps.userId),
191+
}),
192+
component: (props) => <UserBadge name={props.userName} admin={props.isAdmin} />,
133193
});
134194
```
135195

136-
- `fn`: This function is optional. If provided, it receives the props from `<FooterSlots.Bottom />` (e.g.,` { text }`) and allows you to transform them before passing them to `component`. In the example above, `fn` takes `text` and creates a new prop `doubleText`, which repeats the `text` value twice.
137-
- **Without** `fn`: If `fn` is not provided, the props from `<FooterSlots.Bottom />` are passed directly to component without any transformation, one-to-one.
138-
- `component`: This function receives either the transformed props (if `fn` is used) or the original props and renders the component accordingly.
139-
140-
This flexibility allows you to choose whether to modify props or pass them through unchanged, depending on your use case.
141-
142-
### How to Insert Multiple Components into a Slot
196+
### Control rendering order
143197

144-
Inserting multiple components into a slot is straightforward. You can call `footerSlots.insert.into.Bottom` multiple times to add different components. The components will be added in the order in which they are inserted.
145-
146-
#### Example
147-
148-
Here's how you can insert multiple components into the `Bottom` slot:
198+
Components are inserted in any order, but rendered according to `order` value (lower numbers first):
149199

150200
```tsx
151-
footerSlots.insert.into.Bottom({
152-
component: () => <p>First Component</p>,
201+
// This is inserted first, but will render second
202+
slotsApi.insert.into.Sidebar({
203+
component: () => <SecondWidget />,
204+
order: 2,
153205
});
154206

155-
footerSlots.insert.into.Bottom({
156-
component: () => <p>Second Component</p>,
207+
// This is inserted second, but will render first
208+
slotsApi.insert.into.Sidebar({
209+
component: () => <FirstWidget />,
210+
order: 1,
157211
});
158-
```
159-
160-
In this example:
161-
162-
- The first call to `footerSlots.insert.into.Bottom` inserts a component that renders `<p>First Component</p>`.
163-
- The second call inserts a component that renders `<p>Second Component</p>`.
164-
165-
The components will appear in the order they are inserted, so the rendered output will look like this:
166212

167-
```html
168-
<footer>First Component Second Component</footer>
213+
// Result:
214+
// <>
215+
// <FirstWidget /> ← order: 1
216+
// <SecondWidget /> ← order: 2
217+
// </>
169218
```
170219

171-
### How to Manage the Order of Components in a Slot
220+
**Note:** Components with the same `order` value keep their insertion order and all of them are rendered.
172221

173-
You can control the order in which components are rendered within a slot using the optional `order` property. By default, components are added in the order they are inserted. However, you can specify a custom order to rearrange them.
222+
### Defer insertion until event fires
174223

175-
#### Example
176-
177-
Let's build on the previous example and introduce the order property:
224+
Wait for data to load before inserting component. The component won't render until the event fires:
178225

179226
```tsx
180-
footerSlots.insert.into.Bottom({
181-
component: () => <p>First Component</p>,
182-
});
183-
184-
footerSlots.insert.into.Bottom({
185-
component: () => <p>Second Component</p>,
186-
order: 0,
227+
import { createEvent } from 'effector';
228+
229+
const userLoaded = createEvent<{ id: number; name: string }>();
230+
231+
// Component will be inserted only after userLoaded fires
232+
slotsApi.insert.into.Header({
233+
when: userLoaded,
234+
mapProps: (slotProps, whenPayload) => ({
235+
userId: whenPayload.id,
236+
userName: whenPayload.name,
237+
}),
238+
component: (props) => <UserWidget id={props.userId} name={props.userName} />,
187239
});
188-
```
189-
190-
- In this case, the first call inserts `<p>First Component</p>` without an `order` property, so it gets the default position.
191-
- The second call inserts `<p>Second Component</p>` and specifies `order: 0`. This causes the "Second Component" to be rendered before the "First Component".
192240

193-
With the order property applied, the rendered output will look like this:
241+
// Component is not rendered yet...
194242

195-
```html
196-
<footer>Second Component First Component</footer>
243+
// Later, when data arrives:
244+
userLoaded({ id: 123, name: 'John' }); // NOW the component is inserted and rendered
197245
```
198246

199-
#### How the `order` Property Works
200-
201-
- **Type**: `order` is always a number.
202-
- **Default Behavior**: If `order` is not provided, the components are rendered in the order they are inserted.
203-
- **Custom Order**: Components with a lower `order` value are rendered before those with a higher value. If multiple components have the same `order` value, they maintain the order of insertion.
247+
**Note:** You can pass an array of events `when: [event1, event2]` - component inserts when **any** of them fires. Use [once](https://patronum.effector.dev/operators/once/) from `patronum` if you need one-time insertion.
204248

205249
## Community
206250

207-
- [Discord](https://discord.gg/Q4DFKnxp)
208251
- [Telegram](https://t.me/grlt_hub_app_compose)

0 commit comments

Comments
 (0)