December 13, 2022
React: Using children instead of dedicated render slots
Using component props to pass data (or JSX
elements directly) that will later
populate internal UI pieces has been a prevalent pattern in the React community,
trying (probably) to emulate the better-designed slot approach adopted by
Vue.
For example, imagine we are coding a new Nav
component for our site. It will
render a collection of nav items, the page title, and a call to action link.
Each page might customize any of these elements.
Not surprisingly, I think either of these two would be the most popular examples we might find out there (or a combination of both):
// Passing simple data structures with the desired data
<Nav
cta={{ text: "Buy the book!", href: "https://url.to/buy-my-book" }}
menuItems={[
{ text: "Home", href: "/home" },
{ text: "About", href: "/about" },
{ text: "Contact", href: "/contact" },
]}
title="My Site"
/>
// Passing JSX elements directly
<Nav
cta={<a href="https://url.to/buy-my-book">Buy the book!</a>}
menuItems={
<>
<li>
<a href="/home">Home</a>
</li>
<li>
<a href="/about">About</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
</>
}
title={<h1>My Site</h1>}
/>
Even in such as simple example, we can start seeing some of the problems with
this approach: it creeps the component API with render props, it is not very
idiomatic, and it can quickly get worse as we continue adding some more
customization for each element. For example, we may later add specific props to
styling each of the items, title, and call-to-action components:
menuItemStyle
, titleStyle
, ctaStyle
, and so on:
<Nav
cta={{ text: "Buy the book!", href: "https://url.to/buy-my-book" }}
ctaStyle={{ color: "red" }}
ctaOnClick={() => alert("Thanks for buying the book!")}
menuItems={[
{ text: "Home", href: "/home" },
{ text: "About", href: "/about" },
{ text: "Contact", href: "/contact" },
]}
menuItemStyle={{ color: "blue" }}
onMenuItemClick={(href) => alert(`Navigating to ${href}`)}
title="My Site"
titleStyle={{ color: "green" }}
/>
Let’s try a different approach. One that resembles how HTML works and that is supported out-of-the-box by React.
Using children
and dedicated components
<Nav>
<Nav.Title>My Site</Nav.Title>
<Nav.Menu>
<Nav.Menu.Item href="/">Home</Nav.Menu.Item>
<Nav.Menu.Item href="/about">About</Nav.Menu.Item>
<Nav.Menu.Item href="/contact">Contact</Nav.Menu.Item>
</Nav.Menu>
<Nav.CallToAction href="https://url.to/buy-my-book">
Buy the book!
</Nav.CallToAction>
</Nav>
If you have done some React development, you probably know the children
prop
is how React passes content down to components (for example <p>Hola</p>
, where
p
is the component, and Hola
is the value of the children
prop).
The example above uses children
and composition extensively to render the
desired UI. Instead of passing multiple props to the Nav
component, we create
many small, dedicated components which may of course receive their own props.
As a result, this approach gives us a much more flexible and idiomatic API. If
we later need to add additional elements to the Nav
component, we can do so
without changing the Nav
component at all (following the Open-Closed design
principle).
On the other hand, using this technique comes with some drawbacks worth mentioning:
- We have to make it clear that these dedicated components are only meant to be used as children of the
Nav
component. We can do this by using theNav
namespace, for example:Nav.Title
,Nav.Menu
, andNav.CallToAction
. - We must ensure the
Nav
component is flexible enough to handle the different cases we might want to render. For example, if we want to make it possible to render theNav.Menu
component either after or before theNav.CallToAction
component, the styles should be sufficiently sophisticated to handle both cases.
Possible implementation
Pay attention to how we export the Nav
component and its dedicated components.
That’s the secret sauce to namespace them and make it clear they are only meant
to be used as children of the Nav
component.
const Nav = ({ children }) => {
return <nav>{children}</nav>;
};
const CallToAction = ({ children, href }) => {
return <a href={href}>{children}</a>;
};
const Menu = ({ children }) => {
return <ul>{children}</ul>;
};
const MenuItem = ({ children, href }) => {
return (
<li>
<a href={href}>{children}</a>
</li>
);
};
const Title = ({ children }) => {
return <h1>{children}</h1>;
};
Menu.Item = MenuItem;
Nav.CallToAction = CallToAction;
Nav.Menu = Menu;
Nav.Title = Title;
export { Nav };
Notes
- A few weeks after starting to write this post, I received a newsletter from Kent C. Dodds featuring a React article. Coincidentally, the content of the post covered precisely this! So, digging a little more, I found this pattern had been documented since a few years ago (watch out this video talk from Ryan Florence at Phoenix ReactJS Conf in 2017), and it is called Compound components.
- Radix UI uses this pattern extensively.
- If you’re curious, I used this pattern to build the whole landing page for my tddworkshop.com page. You can check the source code here.
Happy hacking!