Mastering React Compound Components: Build Flexible UI with Context and Hooks

This tutorial explains how React compound components work, showing how to share implicit state across parent and child components using Context and Hooks, with practical code examples like a Toggle component and a Reach UI Menu, to create more intuitive and elegant APIs.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
Mastering React Compound Components: Build Flexible UI with Context and Hooks

This article, translated from the original “React Hooks: Compound Components”, introduces the concept of compound components in React, an advanced pattern where multiple components work together, sharing implicit state through an explicit parent‑child relationship.

Compound components enable shared state and logic, allowing developers to create more intuitive and flexible APIs without passing props between child components.

They can be likened to the HTML <select> and <option> elements, where the parent <select> implicitly stores the selected option’s state and shares it with its children.

<select>
  <option value="value1">key1</option>
  <option value="value2">key2</option>
  <option value="value3">key3</option>
</select>

If the tags are used separately, the functionality is incomplete; the combined API is much cleaner than a single <select options="key1:value1;key2:value2;key3:value3"> element that also has to support attributes like disabled.

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

The key idea is “implicit state”: the <select> element stores the selected <option> state internally and shares it with its children without exposing it in the HTML.

Below is a real React example from Reach UI showing a <Menu /> component that provides a compound component API.

function App() {
  return (
    <Menu>
      <MenuButton>Actions <span aria-hidden>▾</span></MenuButton>
      <MenuList>
        <MenuItem onSelect={() => alert('Download')}>Download</MenuItem>
        <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem>
        <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem>
      </MenuList>
    </Menu>
  )
}

In this example, <Menu> encapsulates shared implicit state, which <MenuButton>, <MenuList> and <MenuItem> can access and manipulate, resulting in a more elegant API.

There are two common ways to implement such components: using React.cloneElement on children, or using React Context. This article focuses on the Context‑and‑Hooks approach.

We illustrate the approach with a custom <Toggle> component composed of <ToggleOn>, <ToggleOff> and <ToggleButton />. Clicking the button toggles the displayed content.

function App() {
  return (
    <Toggle onToggle={on => console.log(on)}>
      <ToggleOn>The button is on</ToggleOn>
      <ToggleOff>The button is off</ToggleOff>
      <ToggleButton />
    </Toggle>
  )
}

The effect of the toggle is shown below:

Toggle component demo
Toggle component demo

The full implementation using Context and Hooks is as follows:

import * as React from 'react'
// this switch implements a checkbox input and is not relevant for this example
import {Switch} from '../switch'

const ToggleContext = React.createContext()

function useEffectAfterMount(cb, dependencies) {
  const justMounted = React.useRef(true)
  React.useEffect(() => {
    if (!justMounted.current) {
      return cb()
    }
    justMounted.current = false
  }, dependencies)
}

function Toggle(props) {
  const [on, setOn] = React.useState(false)
  const toggle = React.useCallback(() => setOn(oldOn => !oldOn), [])
  useEffectAfterMount(() => {
    props.onToggle(on)
  }, [on])
  const value = React.useMemo(() => ({on, toggle}), [on])
  return (
    <ToggleContext.Provider value={value}>
      {props.children}
    </ToggleContext.Provider>
  )
}

function useToggleContext() {
  const context = React.useContext(ToggleContext)
  if (!context) {
    throw new Error(`Toggle compound components cannot be rendered outside the Toggle component`)
  }
  return context
}

function ToggleOn({children}) {
  const {on} = useToggleContext()
  return on ? children : null
}

function ToggleOff({children}) {
  const {on} = useToggleContext()
  return on ? null : children
}

function ToggleButton(props) {
  const {on, toggle} = useToggleContext()
  return <Switch on={on} onClick={toggle} {...props} />
}

In the code, a React Context stores and updates the toggle state, while the <Toggle> component provides this context to its child components.

This article aims to give you a solid understanding of compound components in React.

frontendReActhooksCompound ComponentsContext API
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.