You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
<TextFieldLabel>It will be rendered above input</TextFieldLabel>
58
+
<TextFieldLabel>I will be rendered before input</TextFieldLabel>
59
59
<TextFieldTag>Tag 1</TextFieldTag>
60
60
<TextFieldTag>Tag 2</TextFieldTag>
61
61
</TextField>
@@ -70,13 +70,11 @@ export default function App() {
70
70
71
71
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.
72
72
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.
74
74
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.
76
76
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
80
78
81
79
Quoted from [the comment](https://github.com/facebook/react/issues/24979#issuecomment-1193176328) by @devongovett
82
80
@@ -85,9 +83,11 @@ Quoted from [the comment](https://github.com/facebook/react/issues/24979#issueco
85
83
86
84
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.
87
85
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
+
88
88
## Why we want it built in core
89
89
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.
91
91
92
92
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.
93
93
@@ -116,18 +116,145 @@ export default function App() {
116
116
117
117
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.
118
118
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<TextendsReact.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
// valid but nothing will be rendered for `Slot` as it's not wrapped with `Host`
130
+
constComp= () => (
131
+
<div>
132
+
<Slot />
133
+
</div>
134
+
);
135
+
136
+
// valid extension
137
+
constExtendedSlot= (props) => (
138
+
<>
139
+
<Slot prop1="props1" {...props} />
140
+
</>
141
+
);
142
+
143
+
// invalid extension
144
+
constExtendedSlot= (props) => (
145
+
<div>
146
+
<Slot {...props} />
147
+
</div>
148
+
);
149
+
150
+
// valid composition
151
+
constComp1= () => (
152
+
<Host>
153
+
<>
154
+
<Slot />
155
+
<ExtendedSlot />
156
+
</>
157
+
</Host>
158
+
);
159
+
160
+
// invalid composition
161
+
constComp1= () => (
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:
<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 `constTextFieldLabel=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.
120
209
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>)`
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
126
253
127
254
# Alternatives
128
255
129
256
- 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.
131
258
132
259
# Adoption strategy
133
260
@@ -141,4 +268,6 @@ Slots pattern is a native feature of Web Components, other popular frameworks li
141
268
142
269
- Finalise the namings
143
270
- 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