This site runs best with JavaScript enabled.

React Hooks: Compound Components

February 18, 2019

Video Blogger

Photo by karl S on Unsplash


How do compound components change with React hooks?

A few weeks ago I did a DevTips with Kent livestream where I show you how to refactor the compound components pattern from a class component to a function component with React hooks:

If you're unfamiliar with compound components, then you probably haven't watched my Advanced React Component Patterns course on egghead.io or on Frontend Masters.

The idea is that you have two or more components that work together to accomplish a useful task. Typically one component is the parent, and the other is the child. The objective is to provide a more expressive and flexible API.

Think of it like <select> and <option>:

1<select>
2 <option value="value1">key1</option>
3 <option value="value2">key2</option>
4 <option value="value3">key3</option>
5</select>

If you were to try and use one without the other it wouldn't work (or make sense). Additionally it's actually a really great API. Let's check out what it would look like if we didn't have a compound components API to work with (remember, this is HTML, not JSX):

1<select options="key1:value1;key2:value2;key3:value3"></select>

I'm sure you can think of other ways to express this, but yuck. And how would you express the disabled attribute with this kind of API? It's kinda madness.

So the compound components API gives you a nice way to express relationships between components.

Another important aspect of this is the concept of "implicit state." The <select> element implicitly stores state about the selected option and shares that with it's children so they know how to render themselves based on that state. But that state sharing is implicit because there's nothing in our HTML code that can even access the state (and it doesn't need to anyway).

Alright, let's get a look at a legit React component that exposes a compound component to understand these principles further. Here's an example of the <Menu /> component from Reach UI that exposes a compound components API:

1function App() {
2 return (
3 <Menu>
4 <MenuButton>
5 Actions <span aria-hidden></span>
6 </MenuButton>
7 <MenuList>
8 <MenuItem onSelect={() => alert('Download')}>Download</MenuItem>
9 <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem>
10 <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem>
11 </MenuList>
12 </Menu>
13 )
14}

In this example, the <Menu> establishes some shared implicit state. The <MenuButton>, <MenuList>, and <MenuItem> components each access and/or manipulate that state, and it's all done implicitly. This allows you to have the expressive API you're looking for.

So how is this done? Well, if you watch my course I show you two ways to do it. One with React.cloneElement on the children and the other with React context. (My course will need to be slightly updated to show how to do this with hooks). In this blog post, I'll show you how to create a simple set of compound components using context.

When teaching a new concept, I prefer to use simple examples at first. So we'll use my favorite <Toggle> component example for this.

Here's how our <Toggle> compound components are going to be used:

1function App() {
2 return (
3 <Toggle onToggle={on => console.log(on)}>
4 <Toggle.On>The button is on</Toggle.On>
5 <Toggle.Off>The button is off</Toggle.Off>
6 <Toggle.Button />
7 </Toggle>
8 )
9}

You'll notice that we're using a . in our component names. That's because those components are added as static properties to the <Toggle> component. Note that this is not at all a requirement of compound components (the <Menu> components above do not do this). I just like doing this as a way to explicitly communicate the relationship.

Ok, the moment you've all been waiting for, the actual full implementation of compound components with context and hooks:

1import React from 'react'
2// this switch implements a checkbox input and is not relevant for this example
3import {Switch} from '../switch'
4
5const ToggleContext = React.createContext()
6
7function useEffectAfterMount(cb, dependencies) {
8 const justMounted = React.useRef(true)
9 React.useEffect(() => {
10 if (!justMounted.current) {
11 return cb()
12 }
13 justMounted.current = false
14 }, dependencies)
15}
16
17function Toggle(props) {
18 const [on, setOn] = React.useState(false)
19 const toggle = React.useCallback(() => setOn(oldOn => !oldOn), [])
20 useEffectAfterMount(() => {
21 props.onToggle(on)
22 }, [on])
23 const value = React.useMemo(() => ({on, toggle}), [on])
24 return (
25 <ToggleContext.Provider value={value}>
26 {props.children}
27 </ToggleContext.Provider>
28 )
29}
30
31function useToggleContext() {
32 const context = React.useContext(ToggleContext)
33 if (!context) {
34 throw new Error(
35 `Toggle compound components cannot be rendered outside the Toggle component`,
36 )
37 }
38 return context
39}
40
41function On({children}) {
42 const {on} = useToggleContext()
43 return on ? children : null
44}
45
46function Off({children}) {
47 const {on} = useToggleContext()
48 return on ? null : children
49}
50
51function Button(props) {
52 const {on, toggle} = useToggleContext()
53 return <Switch on={on} onClick={toggle} {...props} />
54}
55
56// for convenience, but totally not required...
57Toggle.On = On
58Toggle.Off = Off
59Toggle.Button = Button

Here's this component in action:

So the way this works is we create a context with React where we store the state and a mechanism for updating the state. Then the <Toggle> component is responsible for providing that context value to the rest of the react tree.

I'll walkthrough this implementation and explain the particulars in a future update to my Advanced React Component Patterns course. So keep an eye out for that!

I hope that helps you get some ideas of ways you can make your component APIs more expressive and useful. Good luck!

Discuss on TwitterEdit post on GitHub

Share article
loading relevant upcoming workshops...
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.