Skip to content

Commit ee48eb2

Browse files
committed
update
1 parent 521c2c9 commit ee48eb2

File tree

1 file changed

+142
-13
lines changed

1 file changed

+142
-13
lines changed

text/0000-slots.md

+142-13
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const TextField = (props) => {
2727
const id = useId();
2828

2929
return createHost(props.children, (slots) => {
30-
// library author can create utlis to reduce the boilerplate
30+
// library author can create utils to reduce the boilerplate
3131
const labelSlot = slots.findLast((slot) => slot.type === TextFieldLabel);
3232
const inputSlot = slots.findLast((slot) => slot.type === TextFieldInput);
3333
const tagSlots = slots.filter((slot) => slot.type === TextFieldTag);
@@ -55,7 +55,7 @@ export default function App() {
5555
<div>
5656
<TextField>
5757
<TextFieldInput id="input-id" />
58-
<TextFieldLabel>It will be rendered above input</TextFieldLabel>
58+
<TextFieldLabel>I will be rendered before input</TextFieldLabel>
5959
<TextFieldTag>Tag 1</TextFieldTag>
6060
<TextFieldTag>Tag 2</TextFieldTag>
6161
</TextField>
@@ -70,13 +70,11 @@ export default function App() {
7070
7171
When creating UI library, we have different approaches on the component API design. The most common way is the Configuration pattern used by most UI libraries, everything in one component and provides child props for composition, like `<TextField label helperText />`. It's easy to use, but when we need to to add some extra props to label like `data-testid`, then we have to introduce new props for that which will bloat the api very easily.
7272
73-
Another way to solve this problem is Composition pattern, like `<TextField><TextFieldLabel /><TextFieldInput /></TextField>`, it provides the best flexibility, we are free to customise every part of our component, but it causes another problem: consistency, we have to organise your sub components exactly same order as expected, and the biggest problem is that it's harder to communicate between parent and children.
73+
Another way to solve this problem is Composition pattern, like `<TextField><TextFieldLabel /><TextFieldInput /></TextField>`, it provides the best flexibility, we are free to customise every part of our component, but it causes another problem: consistency, we have to organise your sub components exactly the same order as expected, and the biggest problem is that it's very hard and cumbersome to communicate between parent and children.
7474
75-
For a dedicated Design System, we need consistent ui regardless how we compose it.
75+
For a dedicated Design System, we need consistent ui regardless how we compose it. Here the Slots pattern solve those problem perfectly, we can compose our component with both flexibility and consistency, and it's extreme easy to add A11y support thanks the ability of Inversion of Control. We don't need to using React Context to communicate between parent and children, and no rerendering needed to sync the data.
7676
77-
Here the Slots pattern solve those problem perfectly, we can compose our component with both flexibility and consistency, and it's extreme easy to add A11y support thanks the ability of Inversion of Control.
78-
79-
## Accessible List and Virtualisation
77+
## Accessible List, virtualisation and custom renderer
8078
8179
Quoted from [the comment](https://github.com/facebook/react/issues/24979#issuecomment-1193176328) by @devongovett
8280
@@ -85,9 +83,11 @@ Quoted from [the comment](https://github.com/facebook/react/issues/24979#issueco
8583
8684
Even with the Reach UI's solution, it doesn't work well with SSR. With the Slots pattern, we don't render the children to real DOM but only collect information, so virtualisation is supported by nature.
8785
86+
Also the Slots proposal actually enabled another way of creating custom renderer, we don't need to import the `react-reconciler` which added a lot of bundle size, instead we use the version already bundled in `react-dom` or `react-native`, we define custom tags by `createSlot` and implement custom rendering in `createHost`, an easy showcase is implementing dynamic and extendable declarative configuration.
87+
8888
## Why we want it built in core
8989
90-
I've researched a lot different approaches, none of the current solutions work perfectly without any drawbacks, my [solution](https://github.com/nihgwu/create-slots) is the closest one. But as @devongovett pointed out [here](https://github.com/facebook/react/issues/24979#issuecomment-1205909188), `react-reconciler` is designed to do this job, It would be nice to see it in core, like `react-call-return`.
90+
I've researched a lot different approaches, none of the current solutions work perfectly without any drawbacks, my [solution](https://github.com/nihgwu/create-slots) is the closest one, but as @devongovett pointed out [here](https://github.com/facebook/react/issues/24979#issuecomment-1205909188), `react-reconciler` is designed to do this job, It would be better to have it in core.
9191
9292
It seems `React.Children.forEach` can do the same job demoed in the example above, but there are a lot limitations which are well documented [here](https://github.com/reach/reach-ui/tree/dev/packages/descendants), with this proposal we are free to extend the slot and compose in any way, e.g.
9393
@@ -116,18 +116,145 @@ export default function App() {
116116
117117
The api is very similar to the [deleted](https://github.com/facebook/react/pull/12820) experimental package [`react-call-return`](https://github.com/facebook/react/pull/11364), but with a simpler mental model.
118118
119-
`createSlot<SlotProps>()` creates a slot component and won't be rendered to real DOM but only collect node info, which will be used by `createHost`, for TS/Flow users, `SlotProps` is the props we support in each slot, will be `LabelProps`, `InputProps`, `TagProps` for the example
119+
`createSlot<T extends React.ElementType>(Fallback?: T)` creates a slot component and won't be rendered to real DOM but only collect node info, which will be used by `createHost`. `Fallback` is a fallback component that if the slot is used without HostSlots(outside of `createHost`), it will be fallback to a normal component, which is similar to `slot` property for Web Components, that if the slot is not used in template it will act as a normal component. If `Fallback` is not provided, it will create a pure slot component that nothing will be rendered if it's used without HostSlots. (Not sure about the `Fallback` argument as it's not able to implement with `react-call-return`.)
120+
121+
`createHost(children: React.ReactNode, callback: (slots: React.ReactElement[]) => JSX.Element | null)` mounts the children which hosting slotted components and return the collected slots elements in callback, and then we can render the result to real DOM conditionally. We can find the slots by filter the `slots`(shown in the example above).
122+
123+
For `children` of `createHost`, similar to `react-call-return`, slot component can be extended, wrapped with `Fragment`, but can't be wrapped with Host components(like `div`), see the following use cases
124+
125+
```jsx
126+
const Slot = createSlot();
127+
const Host = (props) => createHost(props.children, (slots) => slots);
128+
129+
// valid but nothing will be rendered for `Slot` as it's not wrapped with `Host`
130+
const Comp = () => (
131+
<div>
132+
<Slot />
133+
</div>
134+
);
135+
136+
// valid extension
137+
const ExtendedSlot = (props) => (
138+
<>
139+
<Slot prop1="props1" {...props} />
140+
</>
141+
);
142+
143+
// invalid extension
144+
const ExtendedSlot = (props) => (
145+
<div>
146+
<Slot {...props} />
147+
</div>
148+
);
149+
150+
// valid composition
151+
const Comp1 = () => (
152+
<Host>
153+
<>
154+
<Slot />
155+
<ExtendedSlot />
156+
</>
157+
</Host>
158+
);
159+
160+
// invalid composition
161+
const Comp1 = () => (
162+
<Host>
163+
<div>
164+
<Slot />
165+
</div>
166+
</Host>
167+
);
168+
```
169+
170+
For nested slots, `createHost` works like peeling the onion, it will only collect the top level slots, nested slots will be handled at next tick if they are included in `callback`'s return, to continue with the very first example:
171+
172+
```jsx
173+
const NestedField = createSlot();
174+
175+
const NestedControl = (props) =>
176+
createHost(props.children, (slots) => {
177+
const textFieldSlot = slots.findLast((slot) => slot.type === NestedField);
178+
const labelSlot = slots.findLast((slot) => slot.type === TextFieldLabel);
179+
180+
return (
181+
<div>
182+
{labelSlot && (
183+
<label
184+
style={{ fontWeight: "bold", color: "red" }}
185+
{...labelSlot.props}
186+
/>
187+
)}
188+
{textFieldSlot && <TextField {...textFieldSlot.props} />}
189+
</div>
190+
);
191+
});
192+
193+
export default function App() {
194+
return (
195+
<div>
196+
<NestedControl>
197+
<TextFieldLabel>I have different style</TextFieldLabel>
198+
<NestedField>
199+
<TextFieldInput />
200+
<TextFieldLabel>Nested field</TextFieldLabel>
201+
</NestedField>
202+
</NestedControl>
203+
</div>
204+
);
205+
}
206+
```
207+
208+
If we support `Fallback` in `createSlot`, then for `const TextFieldLabel = createSlot('label')`, `<TextFieldLabel>Label</TextFieldLabel>` outside of `TextField` will be rendered as `<label>Label</label>`, and in `createHost`'s `callback`, we can return the slot elements directly as after the the onion been peeled, they won't be caught by `createHost` again, so they will fallback to normal components, e.g.
120209
121-
`createHost(children: JSX.Element, callback: (slots: React.Element) => JSX.Element | null)` mounts the children which hosting slotted components and read the collected slots elements, and render the result to real DOM conditionally. We can find the slots by filter the `slots`(shown in the example above), similar to Web Components, there could be nodes not wrapped in slots at all will be treated as unnamed slot(`<slot></slot>)`
210+
```jsx
211+
const Header = createSlot("header");
212+
const Body = createSlot("main");
213+
const Footer = createSlot("footer");
214+
215+
const Layout = (props) =>
216+
createHost(props.children, (slots) => {
217+
const header = slots.find((slot) => slot.type === Header);
218+
const body = slots.find((slot) => slot.type === Body);
219+
const footer = slots.find((slot) => slot.type === Footer);
220+
221+
return (
222+
<div>
223+
{header}
224+
<div>
225+
{body}
226+
{footer}
227+
</div>
228+
</div>
229+
);
230+
});
231+
232+
export default function App() {
233+
return (
234+
<Layout>
235+
<Header>Header</Header>
236+
<Body>Body</Body>
237+
</Layout>
238+
);
239+
}
240+
// will produce the following result:
241+
// <div>
242+
// <header>Header</header>
243+
// <div>
244+
// <main>Body</main>
245+
// </div>
246+
// </div>
247+
```
122248
123249
# Drawbacks
124250
125-
Even we are going to support this feature in a separate package or entry, it will still increase the bundle size of React a bit as we need core support.
251+
- Even we are going to support this feature in a separate package or entry, it will still increase the bundle size of React a bit as we need support from core.
252+
- New concepts and new apis will increase the complexity of learning
126253
127254
# Alternatives
128255
129256
- Bring back `react-call-return` which also could be used to implement this feature [demo](https://codesandbox.io/s/long-hill-os6msf?file=/src/App.js), but the mental model is hard to understand.
130-
- Leave it to userspace to implement with current api, like [create-slots](https://github.com/nihgwu/create-slots), but not very efficient and have some drawbacks, e.g. unable to catch children not wrapped in slots, for list slots we have to force update to get the correct index.
257+
- Leave it to user space to implement with current api, like [create-slots](https://github.com/nihgwu/create-slots), but not very efficient and have some drawbacks, e.g. unable to catch children not wrapped in slots, for list slots we have to force update to get the correct index.
131258
132259
# Adoption strategy
133260
@@ -141,4 +268,6 @@ Slots pattern is a native feature of Web Components, other popular frameworks li
141268
142269
- Finalise the namings
143270
- Do we need to provide internal key for list rendering?
144-
- Adding to a new package or to `React` directly
271+
- Adding to a new package or new entry or exposing from `React` directly?
272+
- Should we support `Fallback` for `createSlot`?
273+
- Do we allow arbitrary content which is not wrapped in slot components? It's supported by Web Components(default slot `<slot></slot>`), but disallowed in `react-call-return`

0 commit comments

Comments
 (0)