-
Notifications
You must be signed in to change notification settings - Fork 559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Slots #223
base: main
Are you sure you want to change the base?
RFC: Slots #223
Conversation
I'm very interested in this proposal, being able to build your own node tree and have it managed by react is very powerful. The string-name based API makes this hard to impossible to statically type. This is something the context API gets right but I feel is missing in this proposal. Do you have any thoughts on this? Maybe a const {Host, Slot} = createSlot() |
@tom-sherman for typings, you will have to do Regarding |
f2e09b7
to
cb90170
Compare
The issue with the string-based API was that there is no easy way to say that a string ID relates to a particular slot type. My proposed API was just an example to demonstrate the concept of having the callsite and the definition site be statically linked which your new API also does but in a different way 👍 It would be good if you could add some examples that address the following scenarios:
|
@tom-sherman Added |
here is the demo to build a tree from Slots, with |
This is an interesting proposal. I'm a relatively uninformed reader of this, so perhaps an ignorant question: I'm trying to understand what is enabled by this that is not possible using props. For example: function MyComponent({ firstEl, lastEl }) {
return <div>{firstEl}<p>Hello</p>{lastEl}</div>;
} The 'slots' here are passed as React elements into the component as props. |
@stefee This is a completely legitimate confusion, I think the RFC needs to improve a bit on the example side to showcase what problem this solves at a glance. My mental model for this problem space is based around the I think it would be worth bringing this example into this RFC pretty much verbatim. Another important note is that this is an API targeted primarily at library developers. I wouldn't expect to see it used too much in apps. Hopefully this explains it better! |
@stefee that's a good question, TLDR: yes you can do it and that's the cheapest approach for app devs, but for ui library authors it's not a good API IMO, and the proposal is much powerful, like A11y and virtualisation support. Think about how would you support A11y in your approach? Consume context in each slot? Which is the approach for most libraries like In terms of Virtualisation, you can find more context in this thread, but you can take it as a tree info collector without rendering the tree to real DOM, so it will enable more possibilities. Also this proposal is much closer to the Slots in Web Components and we don't need to use a static prop( |
@tom-sherman Yes I agree the examples need to be improved, but I'm afraid it will prolong the content a lot if I include the content of https://github.com/reach/reach-ui/tree/dev/packages/descendants#the-problem while I've provide the links there, but probably I should highlight those links to make them more accessible |
Understood, maybe just highlighting the link more in a more prominent position is the way to go. To be clear I wasn't suggesting including the different "options" from that doc, just the problem statement section. I think |
Slots are definitely a feature I miss in React, having worked with Vue and Web Components. First-party support for the Slots API would be great! This RFC would avoid the force-updates and enable slots to be used with React Server Components. |
Just as proof-of-concept, I wrote a very simple Maybe not the best solution, but just something to add into the mix. |
This is an excellent idea. I've been using React for years and I feel like slots could've helped me in a variety of scenarios where it felt too hacky to achieve true composability. It can be done, but it's sometimes not easily, and definitely not clean. I mean, articles like the one from @kentcdodds show the unquestionable benefits of composable components, but trying to build one can make you feel kind of abandoned/unsupported by the framework itself, as if you were going against it. Today I found create-slots and this RFC. I just started using it and I built a clean composable card component in no time. Feels very native and looks so maintainable. Also TS support is excellent. A nice API that solves a real problem. This is the way. TY @nihgwu, I'd love to see this built in React. |
As someone who independently developed my solution for integrating slots into React even before discovering this RFC, I would like to discuss my implementation here to bring a fresh perspective and talk about some other challenges with this and my own design. My implementation focuses on a simplistic and type-safe API, closely resembling implementations from other frameworks, especially Vue.js. Implementationtype ListItemProps = {
children: SlotChildren<
| Slot<"title"> // Shorthand of Slot<"title", {}>
| Slot<"thumbnail"> // Shorthand of Slot<"thumbnail", {}>
| Slot<{ isExpanded: boolean }> // Shorthand of Slot<"default", {isExpanded: boolean}>
>;
};
function ListItem({ children }: ListItemProps) {
const { slot, hasSlot } = useSlot(children); // Slot and hasSlot object inferred from children.
return (
<li>
{/* Render thumbnail if provided, otherwise nothing*/}
<slot.thumbnail />
<div>
{/* Render a fallback if title is not provided*/}
<slot.title>Expand for more</slot.title>
{/* Render the description and pass the prop up to the parent */}
<slot.default isExpanded={dynamicValue} />
</div>
</li>
);
} Here, I used a build plugin to change slot elements into function invocations at build time. This is because slots require access to children with closure and thus need to be created for every component. If this feature were built into React, useSlot would not need to accept children as an argument but would be able to access it with a context-like API, eliminating the need for a custom build plugin. Slots could also just be a function, but syntactic sugar is just too good to say no to. Cool Things (So Far):
Drawbacks (So Far):
Usage from parent
|
@Flammae Thanks for the details comment, will take more time to understand your lib
this is not the case for my impl, I avoided iteration and only rely on React's render mechanism to collect slots, so it just works as long as you don't render extra nodes around slots, so the following still works as tooltip is rendered by const Item = () => <Tooltip><List.Item /></Tooltip>
const list = <List><Item /></List> |
What happens if the tooltip was used this way: <List>
{[
<Item key={1}>First</Item>,
<Item key={2}>Second</Item>
]}
{[
<Item key={1}>Third</Item>,
<Item key={2}>Fourth</Item>
]}
<Item key={1}>Fifth</Item>
<Item>Without key</Item>
</List> And what if the tooltip accepted another slot, Thumbnail, and it was inserted and reordered this way on the subsequent render: <List>
{[
<Item key={2}>Fourth</Item>,
<Thumbnail key={3} />,
<Item key={1}>Third</Item>
]}
<Item key={1}>Fifth</Item>
<Item>Without Key</Item>
{[
<Item key={2}>Second</Item>,
<Item key={1}>First</Item>,
<Item key={3}>New Item</Item>
]}
</List> Would it be able to correctly identify which nodes to update and which nodes to re-render? The correct solution should do this:
Also, if our goal is to not break existing React logic, the items that were previously inside the array and on the subsequent render were outside, or nested deeper, should re-render even if the keys are the same. To clarify, by re-render I mean unmount and remount Edit:I was wrong in thinking React maintains key equality when arrays themselves change positions. What's important is to maintain key equality when arrays stay in the same position and items within the array move. This implementation definitely handles Key equality right!: Here's the test: (npm run test) |
Do we have any further discussion so far? |
Or any plan to implement this in upcoming React release (19, 20..?) |
@will-stone do you have any reason just put thumbs down? |
@dante01yoon There will be lots of people subscribed to this thread, and you just pinged them all, thrice. It’s generally best not to ask for updates on these things, as updates will be posted here when they are ready. Asking for updates is like posting “+1” and doesn’t benefit the discussion. Thanks 🙂 (and no need to reply to this message, which will notify everyone again 😅). |
Agree in some aspect, but someone may want to know any further discussion happens in somewhere place since discussion suddenly hanged since end of last year. As this didn't mean to spam, IMHO we shouldn't stop those want to ask any updates. Anyways, thanks again for your kindness. |
not sure if this helps anything but I had a rather simple implementation of slots like so: import { type FunctionComponent, type PropsWithChildren, type ReactNode, useRef } from 'react'
import { groupBy, mapObjIndexed, path, pipe, prop } from 'ramda'
import type { Fn } from 'src/contexts/types'
export type Slots<T extends string> = {
[S in T]: FunctionComponent<{
/**/
}>
}
export type SlotChildren<T> = T & { children: Iterable<ReactNode> }
export const useGetSlots: <T extends string>(children: Iterable<ReactNode>) => Slots<T> = pipe(
groupBy(path(['type', 'displayName']) as any),
mapObjIndexed((items: ReactNode[]) => () => items[0]) as Fn<Slots<any>>,
useRef,
prop('current') as Fn<Slots<any>>
)
const slotFactory = new Proxy(
{},
{
get<T extends string, P extends keyof Slots<T> & string>(
_target: object,
p: P,
_receiver: any
): Slots<T>[P] {
const comp = ({ children }: PropsWithChildren<Parameters<Slots<T>[P]>>) => <>{children}</>
comp.displayName = p
return comp as Slots<T>[P]
}
}
)
export const useSlots = <T extends string>(): Slots<T> => slotFactory as Slots<T> which can then be used like so in the parent component: const { Header, Main, Side } = useSlots<SidePanelPageSlots>()
return (
<SidePanelPage>
<Header>
<div>Whatever content can go here</div>
</Header>
<Main>Main</Main>
<Side>Side</Side>
</SidePanelPage>
) and SidePanelPage is then defined like so: export type SidePanelPageSlots = 'Header' | 'Main' | 'Side'
export const SidePanelPage = ({ mainTitle, sideTitle, children }: SlotChildren<OwnProps>) => {
const { Header, Main, Side } = useGetSlots<SidePanelPageSlots>(children)
return (
<div>
<div className="header">
<Header />
</div>
<div className="main">
<Main />
</div>
<div className="side">
<Side />
</div>
</div>
)
} since |
In this RFC, we propose a way to support Slots pattern in React
View formatted RFC
Here is a demo how it would work I implemented with
react-call-return
in 10 lines