Sometimes you find yourself in a situation where you need to build a component that is reusable but also very flexible so it can adapt to a myriad of situations. It could be something like a toggle, tabbed navigation, or an accordion. A pattern that I think is fantastic for solving these problems is compound components. That is when you have multiple components working together in a coupled but flexible way.
Let's look at how we could build this accordion as a compound component:
You can find an interactive version of this article with some extracurricular information over here.
This accordion consists of three components: One for the toggling button, one for the collapsible content, and one parent that ties everything together. Here's how it looks when using it:
<Accordion>
<div>
<Accordion.Toggle eventKey="item1">Item 1</Accordion.Toggle>
<Accordion.Content eventKey="item1">Content 1...</Accordion.Content>
</div>
<div>
<Accordion.Toggle eventKey="item2">Item 2</Accordion.Toggle>
<Accordion.Content eventKey="item2">Content 2...</Accordion.Content>
</div>
<div>
<Accordion.Toggle eventKey="item3">Item 3</Accordion.Toggle>
<Accordion.Content eventKey="item3">Content 3...</Accordion.Content>
</div>
</Accordion>
There's a couple of things to note here. First off <Accordion.Toggle>
and <Accordion.Content>
might look a little weird with it if you haven't seen it before, but it is because the <Toggle>
and <Content>
components are added as static properties on <Accordion>
. This is not a requirement in any way but I think it makes it clearer that we are dealing with tightly coupled components intended to be used together.
If you've ever used Reach UI or React Bootstrap this probably looks familiar to you. They use this pattern for accordions and other components.
Secondly neither the <Toggle>
and <Content>
components are direct children of <Accordion>
. This is possible because we use a Context to pass data between our components, which gives us a lot of flexibility in how we want our accordion to look and work.
Here is how the parent component <Accordion>
is implemented:
const AccordionContext = React.createContext()
function Accordion({ children }) {
const [activeKey, setActiveKey] = useState(null)
const setKey = (newKey) => {
if (newKey === activeKey) setActiveKey(null)
else setActiveKey(newKey)
}
return <AccordionContext.Provider value={{ activeKey, setKey }}>{children}</AccordionContext.Provider>
}
There's a bit to unpack here so let's start at the top. We create a context called AccordionContext
that will be used to connect our components. Then we set up some state, activeKey
, to hold the currently open "tab"s key and create a function that updates our state to open/close the accordion. Lastly, we return the component's children
wrapped in our contexts <Provider>
which value we set to an object with activeKey
and setKey
.
With that set up we can take a look at <Content>
where the implementation looks like this:
function Content({ children, eventKey }) {
const { activeKey } = useContext(AccordionContext)
if (activeKey !== eventKey) return null
return children
}
We get activeKey
from the AccordionContext
and if it doesn't match the prop eventKey
we return null
, otherwise we return children
.
Finally, this is how <Toggle>
looks:
function Toggle({ children, eventKey }) {
const { setKey } = useContext(AccordionContext)
return <button onClick={() => setKey(eventKey)}>{children}</button>
}
Here we grab setKey
from our AccordionContext
and when the user clicks the button we call it with the toggles eventKey
as its only argument.
And with that our accordion is complete! You can find the complete implementation here.
So to recap: By splitting the implementation across multiple components that share an implicit state using Context
we've created a more expressive and flexible API for our accordion. If we suddenly decide we want to have the accordion buttons below the content we only need to change the usage and not the implementation. This is one of the biggest benefits of compound components.
Hopefully this has given you some insight into why and when compound components are useful. I think it should be a tool in every React developers toolbox.
You're more than welcome to hit me up on Twitter if you have any suggestions or feedback!
Thanks for reading!
Top comments (0)