HomeAbout Me

Advanced React Patterns 3: Compound Components

By Daniel Nguyen
Published in React JS
June 23, 2025
3 min read
Advanced React Patterns 3: Compound Components

Compound Components

**One liner:** The Compound Components Pattern enables you to provide a set of components that implicitly share state for a simple yet powerful declarative API for reusable components.

Compound components are components that work together to form a complete UI. The classic example of this is <select> and <option> in HTML:

<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>

The <select> is the element responsible for managing the state of the UI, and the <option> elements are essentially more configuration for how the select should operate (specifically, which options are available and their values).

Let’s imagine that we were going to implement this native control manually. A naive implementation would look something like this:

<CustomSelect
options={[
{ value: '1', display: 'Option 1' },
{ value: '2', display: 'Option 2' },
]}
/>

This works fine, but it’s less extensible/flexible than a compound components API. For example. What if I want to supply additional attributes on the <option> that’s rendered, or I want the display to change based on whether it’s selected? We can easily add API surface area to support these use cases, but that’s just more for us to code and more for users to learn. That’s where compound components come in really handy!

For the rest of the exercises in this workshop, we’ll be working with a simple <Toggle /> component.

Every reusable component starts out as a simple implementation for a specific use case. It’s advisable to not overcomplicate your components and try to solve every conceivable problem that you don’t yet have (and likely will never have). But as changes come (and they almost always do), then you’ll want the implementation of your component to be flexible and changeable. One of the most important abilities of a software developer is optimizing for change. Learning how to do that is the point of much of this workshop.

This is why we’re starting with a super simple <Toggle /> component. You’ll be surprised how feature-rich we can make a simple toggle component. Keeping it simple allows us to focus in on making it reusable without getting distracted by the complexities of the feature implementation (like we would if we were building a date picker or something 😅).

Shout-out to Ryan Florence for creating this pattern.

Real World Projects that use this pattern:

  • @radix-ui/react-tabs
  • @radix-ui/react-accordion
  • Actually most of Radix UI implements this pattern

Compound Components

👨‍💼 In this exercise we’re going to make <Toggle /> the parent of a few compound components:

  • <ToggleOn /> renders children when the on state is true
  • <ToggleOff /> renders children when the on state is false
  • <ToggleButton /> renders the <Switch /> with the on prop set to the on state and the onClick prop set to toggle.

We have a Toggle component that manages the state, and we want to render different parts of the UI however we want. We want control over the presentation of the UI.

🦉 The fundamental challenge you face with an API like this is the state shared between the components is implicit, meaning that the developer using your component cannot actually see or interact with the state (on) or the mechanisms for updating that state (toggle) that are being shared between the components.

So in this exercise, we’ll solve that problem by using the 📜 React Context API!

What we want to do in this exercise is allow users of our component to render something when the toggle button is on and to render something else when that toggle button is off without troubling them with managing the state that’s controlling whether it’s shown or not.

Your job will be to make a ToggleContext which will be used to implicitly share the state between these components. The Toggle component will render the ToggleContext and the other compound components will access that implicit state via use(ToggleContext).

🦺 TypeScript might not like your use call depending on how you set up your context. We’ll deal with this in another step.

Compound Components Validation

👨‍💼 Change

to this (temporarily):

import { ToggleButton } from './toggle'
export const App = () => <ToggleButton />

Why doesn’t that work (it’s not supposed to, but can you explain why)? Can you figure out a way to give the developer a better error message that explains what they’re doing wrong and how to fix it?

🚨 The tests will tell you in the message for the error you must throw when the context is undefined.

🦺 Additionally, this is where we can make TypeScript happier (TypeScript knew about the problem we’d run into in this step of the exercise!). Remember, TypeScript isn’t making your life terrible. It’s just showing you how terrible your life already is 😂 In this exercise, we’re going to make our lives better.

(You can go ahead and undo the change to

if you’d like. The tests will let you know that you’ve gotten it right).

app.tsx

import { Toggle, ToggleButton, ToggleOff, ToggleOn } from './toggle.tsx'
export function App() {
return (
<div>
<Toggle>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<ToggleButton />
</Toggle>
</div>
)
}

toggle.tsx

import { createContext, use, useState } from 'react'
import { Switch } from '#shared/switch.tsx'
type ToggleValue = { on: boolean; toggle: () => void }
const ToggleContext = createContext<ToggleValue | null>(null)
export function Toggle({ children }: { children: React.ReactNode }) {
const [on, setOn] = useState(false)
const toggle = () => setOn(!on)
return <ToggleContext value={{ on, toggle }}>{children}</ToggleContext>
}
function useToggle() {
const context = use(ToggleContext)
if (!context) {
throw new Error(
'Cannot find ToggleContext. All Toggle components must be rendered within <Toggle />',
)
}
return context
}
export function ToggleOn({ children }: { children: React.ReactNode }) {
const { on } = useToggle()
return <>{on ? children : null}</>
}
export function ToggleOff({ children }: { children: React.ReactNode }) {
const { on } = useToggle()
return <>{on ? null : children}</>
}
type ToggleButtonProps = Omit<React.ComponentProps<typeof Switch>, 'on'> & {
on?: boolean
}
export function ToggleButton({ ...props }: ToggleButtonProps) {
const { on, toggle } = useToggle()
return <Switch {...props} on={on} onClick={toggle} />
}

Tags

#React

Share

Previous Article
Advanced React Patterns 2: Latest Ref

Table Of Contents

1
Compound Components
2
Compound Components
3
Compound Components Validation

Related Posts

React Testing 8: Testing custom hook
September 09, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media