One liner: The Compound Components Pattern enables you to provide a set of components that implicitely share state for a simple yet powerful declarative API for reusable components.
Compound components are components that work together to form a complete UI. The
classic example of this is <select>
and <option>
in HTML:
<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
The <select>
is the element responsible for managing the state of the UI, and
the <option>
elements are essentially more configuration for how the select
should operate (specifically, which options are available and their values).
Let's imagine that we were going to implement this native control manually. A naive implementation would look something like this:
<CustomSelect
options={[
{ value: '1', display: 'Option 1' },
{ value: '2', display: 'Option 2' },
]}
/>
This works fine, but it's less extensible/flexible than a compound components
API. For example. What if I want to supply additional attributes on the
<option>
that's rendered, or I want the display
to change based on whether
it's selected? We can easily add API surface area to support these use cases,
but that's just more for us to code and more for users to learn. That's where
compound components come in really handy!
Real World Projects that use this pattern:
@reach/tabs
- Actually most of Reach UI implements this pattern
Every reusable component starts out as a simple implementation for a specific use case. It's advisable to not overcomplicate your components and try to solve every conceivable problem that you don't yet have (and likely will never have). But as changes come (and they almost always do), then you'll want the implementation of your component to be flexible and changeable. Learning how to do that is the point of much of this workshop.
This is why we're starting with a super simple <Toggle />
component.
In this exercise we're going to make <Toggle />
the parent of a few compound
components:
<ToggleOn />
renders children when theon
state istrue
<ToggleOff />
renders children when theon
state isfalse
<ToggleButton />
renders the<Switch />
with theon
prop set to theon
state and theonClick
prop set totoggle
.
We have a Toggle component that manages the state, and we want to render different parts of the UI however we want. We want control over the presentation of the UI.
🦉 The fundamental challenge you face with an API like this is the state shared
between the components is implicit, meaning that the developer using your
component cannot actually see or interact with the state (on
) or the
mechanisms for updating that state (toggle
) that are being shared between the
components.
So in this exercise, we'll solve that problem by providing the compound
components with the props they need implicitly using React.cloneElement
.
Here's a simple example of using React.Children.map
and React.cloneElement
:
function Foo({ children }) {
return React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
id: `i-am-child-${index}`,
});
});
}
function Bar() {
return (
<Foo>
<div>I will have id "i-am-child-0"</div>
<div>I will have id "i-am-child-1"</div>
<div>I will have id "i-am-child-2"</div>
</Foo>
);
}
A DOM component is a built-in component like
<div />
,<span />
, or<blink />
. A composite component is a custom component like<Toggle />
or<App />
.
Try updating the App
to this:
function App() {
return (
<div>
<Toggle>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<span>Hello</span>
<ToggleButton />
</Toggle>
</div>
);
}
Notice the error message in the console and try to f