DEV Community

Anton Gunnarsson
Anton Gunnarsson

Posted on • Originally published at antongunnarsson.com

Building flexible and expressive React components

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:

accordion

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>
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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)