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.
The old way of sharing state between the children involved using two React methods:
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 is the parent component that shares the state with all of its children.
cloneElement together to iterate over the
children property. For each child, we clone the element and pass additional properties
setOpenItem that we will use to set the currently active item.
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
AccordionPanel components. It also uses
cloneElement to pass on the props it received from
Accordion to its children.
Here’s the code:
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
This component is responsible for displaying the content of the AccordionItem. It shrinks or expands based on the value of the
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.
First of all, a lot of the code will be similar. The main difference is that rather than using
cloneElement to share props, we use the Context API and create a provider.
Here’s our new implementation of
AccordionContext and use
AccordionContext.Provider to pass
setOpenItem props. Also note how we wrapped
value with useMemo to prevent redundant re-renders.
Here’s the code for
Notice how we no longer need to pass the props received from the Accordion. But we’re still using
cloneElement here to pass the
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.
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.