HomeAbout Me

Advanced React Patterns: Compound Components

By Daniel Nguyen
Published in React JS
July 04, 2025
2 min read
Advanced React Patterns: Compound Components

🧩 Building Flexible UIs with the Compound Components Pattern in React

One-liner: The Compound Components Pattern enables you to create a set of components that implicitly share state, offering a powerful and declarative API for reusable UIs.

šŸ¤” What’s a Compound Component?

A compound component is a group of components that work together to form a cohesive UI unit. If you’ve used native HTML form elements, you’ve already seen this in action:

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

Here, <select> manages the state, and <option> elements describe how the select behaves. The options don’t hold any state themselves, but they rely on the context of the <select> component to work correctly.

This declarative design is what we aim to replicate in React with compound components.


šŸ‘¶ The Naive Approach

Let’s say you want to build your own custom dropdown:

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

It works, but it’s rigid. What if you want to style individual options differently? Or run logic based on which option is selected?

To handle all that, you’d need to bloat your component with extra props and APIs. That’s where the compound component pattern comes in—to give developers control over presentation and flexibility without overcomplicating the API.


šŸŽÆ The Real Goal: Reusability and Flexibility

For the rest of this example, we’ll focus on a simpler case: a <Toggle /> component.

Your typical implementation might look like this:

<Toggle
on={isOn}
toggle={handleToggle}
/>

But what if you want to conditionally render content inside the toggle based on its state? Maybe something like:

<Toggle>
<ToggleOn>āœ… The toggle is ON</ToggleOn>
<ToggleOff>āŒ The toggle is OFF</ToggleOff>
<ToggleButton />
</Toggle>

Here, <Toggle> manages the state, and the child components (<ToggleOn>, <ToggleOff>, and <ToggleButton>) react to that state. This is the compound component pattern in action.


🧠 The Challenge: Sharing State Implicitly

Unlike prop drilling, where data flows explicitly through props, compound components rely on implicit state sharing. The consumer of the API (developer using <Toggle>) doesn’t have to know anything about how the state is managed.

That sounds magical… but it means you (as the library author) must make the state available internally across components.

Enter: React Context.


🧪 Implementation: Creating a Toggle with Compound Components

Let’s walk through what we want to accomplish.

The Compound Components:

  • <Toggle />: the parent that manages state (on) and provides a toggle function
  • <ToggleOn />: shows its children when on === true
  • <ToggleOff />: shows its children when on === false
  • <ToggleButton />: renders a toggle button using internal state and toggle

To make this work, we’ll use React.createContext to create a ToggleContext and use useContext inside each compound component.

// toggle.tsx
const ToggleContext = React.createContext(null)
export function Toggle({ children }) {
const [on, setOn] = useState(false)
const toggle = () => setOn(o => !o)
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
)
}

And inside our compound components:

function useToggleContext() {
const context = useContext(ToggleContext)
if (!context) {
throw new Error(
'Toggle compound components must be rendered within a <Toggle />'
)
}
return context
}
export function ToggleOn({ children }) {
const { on } = useToggleContext()
return on ? children : null
}
export function ToggleOff({ children }) {
const { on } = useToggleContext()
return on ? null : children
}
export function ToggleButton() {
const { on, toggle } = useToggleContext()
return <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
}

Now developers can compose their UI however they want, with <Toggle> managing the logic under the hood.


😱 What If Someone Uses a Component Outside <Toggle />?

<ToggleButton />

This will break! Because there’s no provider above it, the context will be undefined.

We can improve developer experience by throwing a helpful error in useToggleContext() — which we already did above. This makes debugging easy and educates the developer at the same time.


🦺 Making TypeScript Happy

TypeScript will likely warn you that useContext(ToggleContext) might be null. That’s because we initialized the context as null. But thanks to our runtime check (that throws if context is null), we can use TypeScript’s non-null assertion pattern to safely type the context.

Here’s a safer and typed version:

type ToggleContextType = {
on: boolean
toggle: () => void
}
const ToggleContext = React.createContext<ToggleContextType | undefined>(undefined)

Now, TypeScript helps us ensure the context is always properly consumed.


🧱 Why This Pattern Is Powerful

The compound component pattern:

āœ… Promotes composition āœ… Provides flexibility to consumers āœ… Keeps logic encapsulated āœ… Creates clean and declarative APIs

You’ll find this pattern widely used in libraries like:

  • @radix-ui/react-tabs
  • @radix-ui/react-accordion
  • Most of Radix UI Primitives

Shoutout to Ryan Florence who popularized this approach. šŸ™Œ


āœ… Final Thoughts

Don’t jump straight into abstractions when you’re building components. Start with something simple and specific. But as your components grow in complexity and need to support more use cases, consider adopting the Compound Components Pattern.

It will make your components easier to scale, more expressive, and far more enjoyable for others to use.



Tags

#ReactPatterns

Share

Previous Article
Advanced React Patterns: Latest Ref

Table Of Contents

1
šŸ¤” What's a Compound Component?
2
šŸ‘¶ The Naive Approach
3
šŸŽÆ The Real Goal: Reusability and Flexibility
4
🧠 The Challenge: Sharing State Implicitly
5
🧪 Implementation: Creating a Toggle with Compound Components
6
😱 What If Someone Uses a Component Outside <Toggle />?
7
🦺 Making TypeScript Happy
8
🧱 Why This Pattern Is Powerful
9
āœ… Final Thoughts

Related Posts

Advanced React Patterns: Control Props
July 09, 2025
2 min
Ā© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media