|
1 | | -# @beqa/react-slots - Responsible React Parenting (docs are a work in progress) |
| 1 | +# beqa/react-slots - Responsible React Parenting |
2 | 2 |
|
3 | | -@beqa/react-slots brings the slot pattern from Vue and Svelte to React, offering a minimal API that retains all the features you expect while providing unmatched type safety. |
| 3 | +> `react-slots` empowers you to prioritize composability in your component APIs. |
4 | 4 |
|
5 | | -## Why Use the Slot Pattern: |
| 5 | +The core of `react-slots` is the slot pattern. It's designed to provide all the |
| 6 | +features you'd find in Vue and Svelte's slot implementations while keeping |
| 7 | +things familiar for React developers. This slot pattern, complemented by **great |
| 8 | +type inference features** and an **intuitive API for manipulating nodes**, |
| 9 | +allows you to design highly composable components with previously unimagined |
| 10 | +patterns in React. |
6 | 11 |
|
7 | | -- **Structured Children:** With the slot pattern, your component's children become key-value pairs rather than a simple array. This enables you to indicate where different parts of parent-provided slot content should be rendered. |
8 | | -- **Resilience to Change:** Slot-based components are more adaptable to change. You can start with a basic set of children and gradually add conditional rendering, new UI elements, fallback content, and data passing without breaking existing code that uses your components. |
9 | | -- **Convenient APIs:** The library provides additional APIs like the Template's `as` prop, adding convenience to your component development. |
10 | | -- **Fixing React-Specific Problems:** "@beqa/react-slots" addresses React-specific issues like alternatives to React.Children APIs and React.CloneElement that provide solutions you actually want to use. |
| 12 | +## Examples |
11 | 13 |
|
12 | | -While these docs are a work in progress, we recommend checking out [Vue's slots documentation](https://vuejs.org/guide/components/slots.html). You'll easily find corresponding APIs and discover how @beqa/react-slots can help you write better code. |
| 14 | +| The code samples below represent actual implementations. No need to define external state or event handlers for these components to function. | |
| 15 | +| --------------------------------------------------------------------------------------------------------------------------------------------- | |
13 | 16 |
|
14 | | -## Quick Guide: Using Slots to Create Reusable DialogTrigger and Dialog Components |
| 17 | +### Creating highly composable `Accordion` and `AccordionList` components using react-slots |
15 | 18 |
|
16 | | -For a live example, check out this [StackBlitz demo.](https://stackblitz.com/edit/vitejs-vite-pz81vn?file=vite.config.ts,src%2FApp.tsx) |
| 19 | +Checkout |
| 20 | +[live example](https://stackblitz.com/edit/stackblitz-starters-tq32ef?file=pages%2Findex.tsx) |
17 | 21 |
|
18 | | -**Creating the DialogTrigger Component** |
| 22 | + |
19 | 23 |
|
20 | | -```tsx |
21 | | -export type DialogTriggerProps = { |
22 | | - children: SlotChildren< |
23 | | - | Slot<"trigger"> // Content labeled as 'trigger.' |
24 | | - | Slot<{ isOpen: boolean; close: () => void }> // Unlabeled content or labeled as 'default,' with props |
25 | | - >; |
26 | | -}; |
| 24 | +### Creating highly composable `Dialog` and `DialogTrigger` components using react-slots |
27 | 25 |
|
28 | | -export function DialogTrigger({ children }: DialogTriggerProps) { |
29 | | - const [isOpen, setIsOpen] = useState(false); |
30 | | - const { slot } = useSlot(children); // Inferred magic |
| 26 | +Checkout |
| 27 | +[live example](https://stackblitz.com/edit/stackblitz-starters-fa5wbe?file=pages%2Findex.tsx) |
31 | 28 |
|
32 | | - return ( |
33 | | - <div> |
34 | | - <button onClick={() => setIsOpen(true)}> |
35 | | - {/* Render Trigger here or use 'Trigger it' as fallback if no trigger content provided. */} |
36 | | - <slot.trigger>Trigger it</slot.trigger> |
37 | | - </button> |
38 | | - {isOpen && ( |
39 | | - <slot.default isOpen={isOpen} close={() => setIsOpen(false)} /> // Props are passed up to the parent |
40 | | - )} |
41 | | - </div> |
42 | | - ); |
43 | | -} |
| 29 | + |
44 | 30 |
|
45 | | -// Create a type-safe template for DialogTrigger by inferring DialogTrigger slots (optional) |
46 | | -export const dialogTriggerTemplate = |
47 | | - createTemplate<DialogTriggerProps["children"]>(); |
48 | | -``` |
| 31 | +--- |
49 | 32 |
|
50 | | -**Creating the Dialog Component** |
51 | | - |
52 | | -```tsx |
53 | | -export type DialogProps = { |
54 | | - children: SlotChildren< |
55 | | - | Slot // Our header element. Shorthand for Slot<'default', {}> |
56 | | - | Slot<"description", { style: object }> // Slot for 'description' with style prop |
57 | | - | Slot<"primaryAction"> |
58 | | - | Slot<"secondaryAction"> |
59 | | - >; |
60 | | -}; |
61 | | - |
62 | | -export function Dialog({ children }: DialogProps) { |
63 | | - const { slot, hasSlot } = useSlot(children); |
64 | | - |
65 | | - return ( |
66 | | - <dialog open> |
67 | | - <slot.default /> |
68 | | - {/* Render a horizontal line under the header if a header is provided */} |
69 | | - {hasSlot && <hr />} |
70 | | - <slot.description style={{ textAlign: "center" }} /> |
71 | | - <div className="actions"> |
72 | | - <slot.secondaryAction /> |
73 | | - <slot.primaryAction /> |
74 | | - </div> |
75 | | - </dialog> |
76 | | - ); |
77 | | -} |
78 | | - |
79 | | -// Create a type-safe template for the Dialog component (optional) |
80 | | -export const dialogTemplate = createTemplate<DialogProps["children"]>(); |
81 | | -``` |
82 | | - |
83 | | -**Using in Your App** |
84 | | - |
85 | | -```tsx |
86 | | -function App() { |
87 | | - return ( |
88 | | - <DialogTrigger> |
89 | | - {/* Label the span as "trigger" (not type-safe) */} |
90 | | - <span slot-name="trigger">Delete</span> |
91 | | - {({ close }) => ( |
92 | | - // Normally this function would be wrapped in a template element that specifies the 'default' label: |
93 | | - // <template.default>{({close}) => {...}}</template.default> |
94 | | - // but since it's the default slot we can simplify it. |
95 | | - // `close` comes from the dialog's <slot.default close={() => setIsOpen(false)} /> |
96 | | - <Dialog> |
97 | | - Are you sure you want to delete this item? |
98 | | - {/* Type-safe template (the <p> element will be rendered in place of slot.description) */} |
99 | | - <dialogTemplate.description> |
100 | | - {({ style }) => <p style={style}>This action can't be reversed</p>} |
101 | | - </dialogTemplate.description> |
102 | | - {/* Regular template */} |
103 | | - <template.primary> |
104 | | - <button onClick={close}>I understand</button> |
105 | | - </template.primary> |
106 | | - </Dialog> |
107 | | - )} |
108 | | - </DialogTrigger> |
109 | | - ); |
110 | | -} |
111 | | -``` |
112 | | - |
113 | | -## Install the Runtime Library |
114 | | - |
115 | | -```bash |
116 | | -npm i @beqa/react-slots |
117 | | -``` |
118 | | - |
119 | | -## Install the Compile Time Plugin (Optional) |
120 | | - |
121 | | -The compile time plugin is required to transform slot elements returned by useSlot into function invocations as shown below: |
122 | | - |
123 | | -```tsx |
124 | | -// Before transpilation |
125 | | -<slot.default prop1={"foo"} prop2={42}> |
126 | | - Fallback |
127 | | -</slot.default>; |
128 | | -// After transpilation |
129 | | -slot.default("Fallback", { prop1: "foo", prop2: 42 }); |
130 | | -``` |
131 | | - |
132 | | -You have the option to skip installing the compile time plugin and start using slots as functions immediately. |
133 | | - |
134 | | -If your project uses Vite, Rollup, or esbuild, you can install `@beqa/unplugin-transform-react-slots` and follow the configuration steps for your bundler. For other bundlers or if you are using Babel in your project, you should install `@beqa/babel-plugin-transform-react-slots`. Note that you don't need to install both plugins. |
135 | | - |
136 | | -<details> |
137 | | - <summary><strong>Babel Plugin</strong></summary> |
138 | | - |
139 | | -```bash |
140 | | -npm i @beqa/babel-plugin-transform-react-slots |
141 | | -``` |
142 | | - |
143 | | -Add react-slots plugin to your babel config |
144 | | - |
145 | | -```js |
146 | | - // babel.config.json |
147 | | - { |
148 | | - "plugins": {"@beqa/babel-plugin-transform-react-slots"} |
149 | | - } |
150 | | -``` |
151 | | - |
152 | | -</details> |
153 | | - |
154 | | -<details> |
155 | | - <summary><strong>Vite Integration</strong></summary> |
156 | | - |
157 | | -```bash |
158 | | -npm i @beqa/unplugin-transform-react-slots |
159 | | -``` |
160 | | - |
161 | | -Add the `unplugin.vite` to your Vite configuration (vite.config.js) before the react plugin: |
162 | | - |
163 | | -```js |
164 | | -// vite.config.js |
165 | | -import unplugin from "@beqa/unplugin-transform-react-slots"; |
166 | | -import react from "@vitejs/plugin-react"; |
167 | | - |
168 | | -export default { |
169 | | - plugins: [unplugin.vite(), react()], |
170 | | -}; |
171 | | -``` |
172 | | - |
173 | | -</details> |
174 | | - |
175 | | -<details> |
176 | | - <summary><strong>Esbuild Integration</strong></summary> |
177 | | - |
178 | | -```bash |
179 | | -npm i @beqa/unplugin-transform-react-slots |
180 | | -``` |
181 | | - |
182 | | -Add `unplugin.esbuild` to your plugins list in your esbuild config |
183 | | - |
184 | | -```js |
185 | | -import unplugin from "@beqa/unplugin-transform-react-slots"; |
186 | | - |
187 | | -// esbuild.config.js |
188 | | -await build({ |
189 | | - plugins: [unplugin.esbuild()], |
190 | | -}); |
191 | | -``` |
192 | | - |
193 | | -</details> |
194 | | - |
195 | | -<details> |
196 | | - <summary><strong>Rollup Integration</strong></summary> |
197 | | - |
198 | | -```bash |
199 | | -npm i @beqa/unplugin-transform-react-slots |
200 | | -``` |
201 | | - |
202 | | -Add the `unplugin.rollup` to your plugins list before all other plugins in your Rollup configuration (rollup.config.js): |
203 | | - |
204 | | -```js |
205 | | -import unplugin from "@beqa/unplugin-transform-react-slots"; |
206 | | - |
207 | | -// esbuild.config.js |
208 | | -await build({ |
209 | | - plugins: [unplugin.rollup()], |
210 | | -}); |
211 | | -``` |
212 | | - |
213 | | -</details> |
214 | | - |
215 | | -<details> |
216 | | - <summary><strong>Performance Optimization with Unplugin Options</strong></summary> |
217 | | - |
218 | | -```tsx |
219 | | -type Options = { |
220 | | - include: RegEx; |
221 | | - exclude: RegEx | RegEx[]; |
222 | | -}; |
223 | | - |
224 | | -const options = { |
225 | | - include: /\.(tsx)|(jsx)|(js)/, |
226 | | -} satisfies Options; |
227 | | - |
228 | | -unplugin.yourBundler(options); |
229 | | -``` |
230 | | - |
231 | | -`unplugin-transform-react-slots` is designed to be fast at finding and transforming React slots. By default, it checks every JavaScript (js), JSX (jsx), and TypeScript (tsx) file in your project, excluding files in the node_modules directory. However, you can optimize its performance further by using specific options. |
232 | | - |
233 | | -**include Option** |
234 | | - |
235 | | -If you have other tools configured in a way that JSX syntax is only used in certain files, you can provide the include regular expression (RegEx) as an argument to your plugin. For instance: |
236 | | - |
237 | | -```tsx |
238 | | -unplugin.yourBundler({ include: /\.(tsx)|(jsx)/ }); |
239 | | -``` |
240 | | - |
241 | | -With this configuration, the plugin will only check .tsx and .jsx files in your project, improving performance by skipping unnecessary files. |
242 | | - |
243 | | -**exclude Option** |
244 | | - |
245 | | -Additionally, you can use the exclude option to exclude specific files or directories from being processed. This can be useful for excluding configuration files or large files that don't need slot transformation: |
246 | | - |
247 | | -</details> |
248 | | - |
249 | | -## Troubleshooting |
250 | | - |
251 | | -``Unsupported syntax: `useSlot` or an object holding a nested `useSlot` value used inside ... `` |
252 | | - |
253 | | -If you encounter this error message after initializing the compile-time plugin for your project, it likely indicates that the plugin is applied after React elements have already been transpiled. To resolve this issue, you should adjust your configuration to ensure that the plugin runs before other syntax transformations. |
254 | | - |
255 | | -This error occurs when useSlot or the return value of useSlot is used in a way that could potentially mutate slots before they are used. It's important to note that this is a specific error related to the compile-time plugin. |
256 | | - |
257 | | -If you wish to disable the transformation for a specific file where this error occurs, you can add the following comment at the beginning of the file: |
258 | | - |
259 | | -```js |
260 | | -// @disable-transform-react-slots |
261 | | -``` |
262 | | - |
263 | | -After adding this comment, you should only use the function signature of slots in that file. |
| 33 | +| If you like this project please show support by starring it on [Github](https://github.com/Flammae/react-slots) | |
| 34 | +| --------------------------------------------------------------------------------------------------------------- | |
0 commit comments