In this post, let’s take a look at a popular React pattern – Compound Component. This pattern allows writing complex components in a clean and declarative way. It proved to be so useful that almost all of the popular React UI libraries, such as Material UI, Semantic UI, Chakra UI, and many others, ended up adopting it.
This post will include the older way of building compound components and the newer version that uses hooks and Context API.
I’ll be writing components using the Emotion styling library and TypeScript.
What is the Compound Component pattern?
As mentioned before, the Compound components pattern allows writing a declarative and flexible API for complex components. You build the component using multiple loosely coupled child components. Each of them performs a different task, yet they all share the same implicit state. When put together, these child components make up our compound component.
You have, no doubt, encountered compound components before when working with a UI library. Take a look at this snippet:
|
|
Does the structure look familiar? That’s what the API typically looks like for compound components.
Here’s the structure of the Accordion component at a high level:
As a side note, a lot of UI libraries use .
for their compound component API like so:
|
|
This approach is strictly optional and you can write your component however you prefer. It doesn’t affect the end result in any significant way.
Why use the Compound Component pattern?
To reiterate, the main advantage of building complex components this way is how easy it is to use them. Thanks to the implicit state, the inner workings of the compound component are hidden from the clients. At the same time, the clients get the flexibility to rearrange and customize the child components in any way they please.
Notice how we declaratively listed the content of the Accordion yet didn’t have to meddle with its inner state.
Accordion
will handle all of the inner state logic, including shrinking and expanding items on click. All we had to do is list the items in the order we want.
So let’s go over the advantages of using the Compound Component pattern one last time:
- The API for your component is declarative.
- Your child components are loosely coupled. That makes it easy to reorder, add and remove the child component without affecting its siblings.
- Much easier to style and change the design of the component.
Now that you’re hopefully convinced to give this thing a shot, let’s start with the tutorial. We will build the Accordion
we’ve been talking about this whole time.
As promised, we’ll go over both the older and the newer approaches.
Older approach
Helper Methods
The old way of sharing state between the children involved using two React methods: React.Children.map
and cloneElement
. Let’s cover them briefly.
React.Children.map
lets you iterate over the children
property of your component the same way you would iterate over an array. According to the official docs: “Invokes a function on every immediate child contained within children with this set to thisArg. If children is an array it will be traversed and the function will be called for each child in the array.”
cloneElement
lets you create a copy of any React element and pass any additional props you like. According to the docs: “Clone and return a new React element using the element as the starting point.”
Accordion
Accordion
is the parent component that shares the state with all of its children.
|
|
We use React.Children.map
and cloneElement
together to iterate over the children
property. For each child, we clone the element and pass additional properties openItem
and setOpenItem
that we will use to set the currently active item.
AccordionItem
AccordionItem
is interesting in that it’s both a parent and a child. It’s a child of the Accordion
component, but it’s also a parent to the AccordionHeader
and AccordionPanel
components. It also uses cloneElement
to pass on the props it received from Accordion
to its children.
Here’s the code:
|
|
AccordionHeader
This component will register the onClick
event that will allow us to shrink/expand each item. For that, we use the setOpenItem
method received from AccordionItem
.
|
|
AccordionPanel
This component is responsible for displaying the content of the AccordionItem. It shrinks or expands based on the value of the openItem
property.
|
|
And that’s all there’s it to it! This implementation works, but it limits us on how we can structure the children. For example, our API will break if you try using some other elements as children of Accordion
like so:
|
|
Here’s the error:
The reason for it is that React.Children.map
can only iterate on the direct children of the Accordion
, so nesting children in a div
won’t work.
We can do better. Let’s look at the newer implementation that uses the Context API instead.
Newer approach
First of all, a lot of the code will be similar. The main difference is that rather than using React.Children.Map
and cloneElement
to share props, we use the Context API and create a provider.
Accordion
Here’s our new implementation of Accordion
:
|
|
We create AccordionContext
and use AccordionContext.Provider
to pass openItem
and setOpenItem
props. Also note how we wrapped value
with useMemo to prevent redundant re-renders.
AccordionItem
Here’s the code for AccordionItem
:
|
|
Notice how we no longer need to pass the props received from the Accordion. But we’re still using cloneElement
here to pass the id
prop.
AccordionPanel & AccordionHeader
These child elements now pull the shared state directly from the context using the useContext
hook. Here’s the implementation of both of them:
|
|
Overall, this implementation has less code and better performance. That’s because cloneElement
causes a slight performance penalty.
However, the main advantage is that we have a much more flexible API for our Accordion, and the example code that broke earlier now works just fine.
Conclusion
Components build with the Compound Component patterns are a joy to use. Using this pattern, you’re better equipped for constant changes in design and functional requirements. Something that happens a lot when building software. I hope you found this post useful!
Thanks to Ken C. Todds for this great article that explains the concept.
Here’s the link to the CodeSandbox with the full code of this tutorial.
If you’d like to get more web development, React and TypeScript tips consider
following me on Twitter,
where I share things as I learn them.
Happy coding!