Compound Component Pattern and React Context

How to Implement Compound Component Pattern with React Context

Vladimir Topolev
Enlear Academy

--

Compound Component Pattern is another pattern commonly used in React, and this pattern allows to build components using multiple loosely coupled child components. They perform different tasks but all together operate against a shared implicit state (figure 1).

I know that there are many articles that covered this topic, but be patient before closing it. At the end of the article, we try to reveal some mystery about React Context since it turns out that not everyone knows how it works.

Figure 1 — Structure of Compound Component

There are at least two options for implementing the Compound component pattern and the main difference between them — which way we choose to propagate implicit state from Compound Component to their coupled child components. We review only one with the usage of React Context, which is more flexible.

Another way to propagate implicit states is using React Top-Level API: React.Children.map (doc link) together with React.cloneElement (doc link), if you’re interested in getting more details this way, don’t hesitate to read this amazing article.

What are we going to implement?

We will implement a simple Accordion component applying a Compound Pattern. Structure and a bunch of future child components are presented in figure 2. Accordion should handle all of the inner state logic, including shrinking and expanding items on click.

Figure 2 — Structure of Accordion components

Name conventions

As you may mention from figure 1, figure 2, all child components which are supposed to work alongside a Compound Component have names with dot notations: Accordion.Header, Accordion.Content Accordion.Item , etc. It’s not mandatory to follow this rule, but it helps to understand that all those components are a part of Accordion component and are supposed to work together.

Implementing Compound Component Pattern

First of all, let’s define the structure of the components that we are going to implement:

1. Accordion component

It’s a wrapper for all our components. As we discussed earlier, this component is the main keeper of implicit state, and state property will be propagated if needed for all loosely coupled child components. Propagation will be completed via React Context. Out context will content only two properties: id of a current active item which should be expanded, and handler, which allows changing the id of the current item. Let’s implement this:

2. Accordion.Item component

Here you should mention that we need a way to distinguish one Accordion item from another. Therefore a developer should define a mandatory unique id field for each Accordion.Item component.

There’s one issue here, Accordion.Item component contents another compound components, and we need a way to propagate its id field. But nobody actually restricts you to create another AccordionItem React context is responsible for propagating Item id to all their children components (figure 3).

Figure 3 — Structure of React Contexts

Well, therefore Accordion.Item should only declare new React Context, pass id field in it, and render all children components. Let’s implement this:

3. Accordion.Header component

This component should just handle changing of the currently active accordion item by a user click; all needed properties are extracted from both contexts:

4. Accordion.Content component

This component is responsible for whether we need to show content or now dependently which current active item is. All necessary properties are extracted from both React contexts:

All code together is provided in this repository
Here demo:

How React Context Works

There’s a place where you may catch anybody and understand that they won't understand how React Context actually works. You may do this if anybody asks one question after reading this article. What is this question:

Let’s imagine, that we put two accordions on one screen or even put one accordion into another one. Is it going to work? Is it possible that contexts may conflict between each other?

Spoiler — yep, it’s going to work, and contexts are not going to conflict with each other; here is the proof:

The issue here is that when we learn React Context following tutorials and documentation, Context is used as a global state of the app. For example, there may be contexts responsible for light/black CSS theme mode or for keeping authorized user info. However, there is usually only one instance of each context within one app. And when open React Context documentation (link), it contains confusing phrases like: “Context is designed to share data that can be considered “global” for a tree of React components.

It’s a pretty fair question and concern whether React app may contain more than one instance of one Context type in one React Tree. As you may already understand, that the answer is:

React App may content more then one instance of one context type within one React Tree.

Let’s investigate how it works:

Here we create a bunch of nested Providers with the same instance of Context and some Consumers. Here you should mention some crucial points (figure 4):

  • all Context has their own values, and they don’t conflict one with another despite that they were created with one React.createContext factory function
  • any Consumer may extract values from one Context type (created with one React.createContext) which is the nearest along with tree structure — React just traverses all parent tree nodes and as soon as finds the first parent node where Context with particular type attached — returns this value.
Figure 4 — How React Context works

Well, who already worked with material-ui, probably recognize this pattern when we may nest as many ThemeProviders as we want to customize each part of the app (link):

<ThemeProvider theme={outerTheme}>
<Checkbox defaultChecked />
<ThemeProvider theme={innerTheme}>
<Checkbox defaultChecked />
</ThemeProvider>
</ThemeProvider>

--

--

Addicted Fullstack JS engineer. Love ReactJS and everything related to animation