Building Compound Components in React

Ben Giles
Unibuddy
Published in
7 min readMar 29, 2022

As part of the Design System (DS) squad at Unibuddy, my day-to-day role as a Software Engineer extends to creating and improving our internal design system, defining the requirements of DS components, and implementing best practices around accessibility, documentation, and testing.

As you might have also experienced, building DS components involves thinking extensively about a component’s API and how it can be implemented. I’ve learned the hard way that components with cluttered APIs and tightly coupled use cases are difficult for engineers to get their heads around.

Photo by Jason Strull on Unsplash

We’ll start by looking at an example of how to utilize the compound component pattern that helps overcome these issues (such as tightly coupled use cases and cluttered APIs) by building an accordion menu in a way that meets accessibility and best practice standards.

Component Implementation:

Let’s implement a new accordion component. For now, we won’t worry about how it's composed or any specific design patterns, we’re just concerned that it looks like this:

The accordion component we’ll be building today

Ok, let’s get to it! We’ve got a tight deadline and just need to get something working that has some semblance of an accordion when you click on it. I imagine that most of us would create something like this at first:

Our job is done! Well, aside from a few glaring best practice and accessibility issues. Time to grab the next ticket from the backlog. Right?

The Accordion component in practice.

Well, what would happen if I wanted to also pass through a defaultExpanded prop?

Adding a defaultExpandedprop...

And maybe we need to have a onClick handler too…

Now a custom onClick handler….

What about a custom className

Can we stretch to a custom className….

What happens if another team wanted to use this component and enable all of the accordion items to open at once. That's another prop, right? Suddenly we have a component that has an extensive and expanding list of possible props as potential use cases stack up.

This ticket now has a lot of moving parts just for what we thought would be a simple component.

Why is this always so complex?

Fortunately, there’s a cleaner way to do this.

Introducing the Compound Component pattern:

Let’s discuss the Compound Component pattern. The objective of the compound component pattern is to have a clearer, more expressive, and flexible API. Kent C Dodds succinctly explains them as:

Think of compound components like the <select> and <option> elements in HTML. Apart they don't do too much, but together they allow you to create the complete experience. The way they do this is by sharing implicit state between the components. Compound components allow you to create and use components which share this state implicitly.— Kent C. Dodds

So perhaps our original implementation can be improved to use the Compound Component pattern. Let’s think about the example above. The <option> element is a child of the <select> element, it's not hidden, but we know that state is shared implicitly between them.

Select and Option example — sharing implicit state

Following the same pattern, we need to think about having a contextual wrapper around the child components, so that we can also pass implicit state between the parent and child.

Ideally, the end goal would be to have an Accordion component that can be created like the image below. As you can see, the state will have to be passed implicitly between the two, and the only thing we as engineers care about is how many AccordionItems are included.

Desired Accordion implementation — let’s do this!

So, how can we do this in React? There are several React APIs that come to mind:

React Children

We could use React.Children.map, where we map over an array of child components, passing down props to them using the React.cloneElement API. This could be great, all of the accordion’s children would get the props we pass down and implicit state would be shared between parent and child components.

However, there’s one notable drawback to this method. It lacks the flexibility we require in that we can only pass props down to the direct children of the parent component (which means you’ll either have to do another clone or prop drill). This wouldn’t be that useful if our AccordionItem component was made up of several custom sub-components (e.g. layout components such as spacers, intermediate containers, etc) as these subcomponents would also need to pass state down or use contextual props.

What else could we use?

React Context

We may instead have more luck with a different React API called Context. Context differs from using React.Children in that is designed to share data with an entire tree of React components, rather than direct descendants. This means that we can create a context specifically for the Accordion, which would enable the state to be consumed by any of its child components if we chose to do so.

Accordion Implementation:

Ok, so now we’ve decided on the React API we want to use, we can now implement the Accordion component (which you can see below).

You’ll see that we have created an AccordionContext using the createContext method provided by the context API. The AccordionContext comes with a Provider method which allows components to access contextual changes via the value prop. The AccordionContext then wraps the children to enable subscription to its context.

We’ve also implemented a number of hooks from the React API in the Accordion component for state management and performance :

  • useState creates a stateful value (activeItem) and a function to update it (setActiveItem). In this case, activeItem represents the active accordion item that’s been selected. This also forms the basis of the implicit state we are passing between parent and child components, and takes a default value (defaultExpanded), which determines whether that accordion item should be expanded to the end-user.
  • useCallback provides a hook that returns a memoized callback called setToggle, which controls a user clicking on an individual accordionItem. The use of useCallback here is to prevent unnecessary and expensive child components from re-renders on state change.
  • Finally, useMemo creates a memoized value. Like useCallback this will only create a new instance of itself if the dependency array has changed — it's not strictly needed in this instance, but it's helpful to prevent expensive and unnecessary re-rendering. Although perhaps in this example it’s a little contrived, as with the use of useCallback.

So, just to recap the Accordion component is now a contextual wrapper of its children. It manages state, which maintains a record of the activeItem and utilizes several memorization methods to optimize the performance of our React component for the AccordionContext provider.

AccordionItem:

Now that we’ve implemented our contextual wrapper, the Accordion, we need to implement the individual accordion items so that they are able to subscribe to the AccordionContext correctly.

To do this we have created a helper method called useAccordionContext which is a custom hook enabling our child components to access the AccordionContext via the React useContext API. If you look closely, we’ve included a defensive strategy of checking that the hook returns a context correctly, if not we throw an Error to warn the engineer implementing the code that the use of this function is out of the scope of the provider.

Once we’ve extracted the context successfully we then use the AccordionContext in a number of ways within the AccordionItem.

  • The stateful value activeItem is provided to the component to determine if the ExpandableSection should be shown by default.
  • The setToggle is used to determine the accordion item that has been toggled via the onClick handler.
  • We are also able to determine whether or not an accordion item should have a background color via the striped prop (you can see the implementation of this in the code sandbox below).

And that's it! We’ve just created a working Accordion component, with multiple children, implemented using the Compound Component pattern which utilizes a number of complex React Hooks and APIs 😅.

Have a look at the working implementation in the code sandbox below.

Our working Accordion component

In Review:

The Accordion component uses React Context to store the current state, the state update function, default expanded state, and whether the accordion should have a color contrast between odd and even child components. We can add as many children as we want (including dumb components which have no access to the accordion context), without the need to change the API all the time or add any further props to it, and all the time state is shared implicitly between them.

This is the main principle and benefit of compound components — extensible, easy to use, and flexible APIs.

Further Reading:

If like me, it took you a little while to get your head around some of the more complex patterns in React, why not check out some of these other great resources that helped me out:

We’re still hiring engineers!

Unibuddy’s growing and if you’d like to work for one of the fastest-growing ed-tech firms feel free to look at our open engineer vacancies here.

--

--