Unpacking the Slot Component
This post is inspired by my recent experience using Shadcn UI. I wanted to dive deeper into how each component works, starting with the Button. However, to truly understand Shadcn UI, it's crucial to explore the components of Radix UI, as Shadcn UI is built on Radix UI. Therefore, I decided to start with one of the key features in Radix UI: the Slot component.
The code in this post is a simplified version to help understand the Slot component. Also, please note that this post is based on the Radix UI source code as of 13 August 2024, so there might be updates in the future I haven't covered.
Meet the Slot
What it is and why it matters
According to the official Radix documentation, Slot
is defined as:
Merges its props onto its immediate child.
In other words, the Slot component copies and pastes its props onto its child. So why does this matter? Seeing a concrete example can be helpful. Let's check out a sample from Radix UI.
So, how will this app appear? Because the asChild
prop is used, Slot takes over as the top-level element instead of the button. Since Slot merges props onto its direct child, only the <a> tag will be rendered, including any props passed to the Button.
Pretty neat. With the asChild prop, Slot can turn into any element you need. Essentially, it lets you decide how things get rendered.
Composition
Radix UI describes this approach as "Composition," a fundamental concept in software development. The Button component is a great example of this in action. Using the asChild prop, your component has a Radix functionality built into it. This means you can leverage Radix's capabilities while customizing the element to fit your needs, giving you more flexibility and control.
How Slot works
We'll look at the code in a very abstract way. I hope this makes it easier to grasp the actual code when you see it.
Slot and SlotClone
First, we'll examine Slot and SlotClone without the branching for Slottable.
Remember how I described Slot as just copies and pastes? SlotClone
is handling that part.
Let's go one step deeper. The main focus is on merging props and composing refs. The rest is about handling exceptions. One exception to keep in mind is that SlotClone expects only one child element.
Merging props, event handlers, and more
We'll see how merging works. The mergeProps
function covers props, event handlers, styles, and class names. The main point to remember is that the child always takes priority.
It's easier to understand with JSX expressions.
Child props override Slot props.
Child event handlers run before Slot handlers.
Child styles replace Slot styles.
Child classNames have higher priority.
Composing ref
Let's move on to refs. It's already pretty straightforward, but we can flatten it out to see how it works.
You can see how this uses a closure. The composeRefs
function takes any number of refs and returns a new function. This function then takes a node and sets each ref to that node.
Here's how it looks with JSX. There's no priority between slotRef and childRef since both are applied to the div.
Slottable and isSlottable
You've seen the basic version of Slot above. Now, we'll see how the Slottable
component changes things.
Let's compare them side by side.
Without Slottable, Slot expects just one child element. So, if you have more than one child, you'll need to group them, which can be less flexible.
With Slottable, you can have multiple children instead of just one. Plus, you can pick out and manage the slottable ones separately. It gives you the flexibility to place slottable components inside the Slot.
Now that we're familiar with this, let's take another look at Slot. First, it checks if there's a slottable child. If there is, it uses SlotClone to handle just that slottable child. Otherwise, it passes children to SlotClone as is.
How can you tell if something is slottable? The children are wrapped with Slottable, and a isSlottable
function is used to check if they're slottable.
Side note
So, how does that sound so far? I hope this walk-through has been clear and helpful. If you spot any mistakes or have feedback, please reach out! I'd love to hear from you.
For further insights, check out the source code or look into the test code and stories. Also, if you're ready to dive in, Radix offers a detailed guide on using this approach and avoiding potential pitfalls.
References
- https://www.radix-ui.com/primitives/docs/utilities/slot
- https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx
- https://www.radix-ui.com/primitives/docs/guides/composition
- https://medium.com/@bryanmylee/aschild-in-react-svelte-vue-and-solid-for-render-delegation-645c73650ced
- https://mjsdo.hashnode.dev/radix-slot (Korean)