A single component's prop list can explode as it needs to support more and more customization. The Compound Components pattern replaces that with several cooperating sub-components, coordinated invisibly through Context. This lesson covers building a Select-style compound component from scratch.
1The Prop Explosion Problem
A component like Select that needs custom option rendering, icons, and trigger labels quickly accumulates an unwieldy list of configuration props. The Compound Components pattern addresses this by letting consumers compose the internal structure directly as JSX, rather than configuring every variation through props on one monolithic component.
2What Compound Components Look Like
Instead of one component, several small, related components are exposed together — like Select, Select.Option, and Select.Trigger — meant to be composed as JSX by the consumer. This gives the consumer full control over structure and content, while shared state is coordinated behind the scenes.
3Sharing State with Context
Sub-components need to coordinate — an Option needs to know the currently selected value and how to change it — without the consumer manually threading that data through props. The parent component creates a Context and provides shared state and setters, which each sub-component reads internally.
4Consuming Context Inside a Sub-Component
Each sub-component calls useContext to read the shared state and update functions provided by the parent. This coordination is invisible to the consumer, who only writes plain JSX, while internally it's exactly what lets the separate sub-components cooperate correctly.
5Attaching Sub-Components with Dot Notation
Assigning Select.Option = Option attaches the Option component as a property of the Select function, making <Select.Option> valid JSX. This groups related components under a single, discoverable namespace and lets consumers import just the parent component to access everything.
6Step-by-Step Breakdown
The Prop Explosion Problem. A <Select> component that needs custom option icons, custom styling per option, and a custom trigger label quickly grows an unwieldy prop list: optionIcon, optionRenderer, triggerLabel, optionClassName... Compound Components solve this by letting consumers compose the internal pieces directly, instead of configuring them all through props on one giant component.
What Compound Components Look Like. Instead of one monolithic component, you expose several small components meant to be used together, like <Select>, <Select.Option>, and <Select.Trigger>. The consumer composes them as JSX, giving them full control over structure and content, while the parent silently coordinates shared state behind the scenes.
What's the main advantage of <Select><Select.Option /></Select> over <Select options={[...]} /> for highly customizable UI?
- →Each option can be arbitrary, fully customized JSX instead of a rigid config object
- →It's always fewer total characters to type
Sharing State with Context. The sub-components need to coordinate — Select.Option needs to know the currently selected value and how to change it, without the consumer having to pass that down manually. The parent Select component creates a Context, and each child sub-component reads from it internally.
Consuming Context Inside a Sub-Component. Each sub-component, like Select.Option, calls useContext(SelectContext) to read the shared state and update function. From the consumer's perspective, this is invisible — they just write JSX. From the implementation's perspective, it's what makes the pieces actually cooperate.
How does Select.Option know which value is currently selected, without the consumer passing it a selectedValue prop directly?
- →It reads shared state from Context provided by the parent Select
- →It reads a global window variable
Attaching Sub-Components with Dot Notation. Select.Option = Option is what makes <Select.Option> valid JSX — you're attaching the Option component as a property on the Select function itself. This groups related components under a single, discoverable namespace, making the relationship between them explicit at the import site.
Mastery Achieved. You now understand the Compound Components pattern: exposing several cooperating sub-components instead of one prop-heavy monolith, sharing state invisibly through Context, and attaching sub-components via dot notation for a single clean import. Next, you'll compare controlled and uncontrolled component design more broadly.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Compound Components Are a Great Fit for Accessible Widget Patterns
ARIA widget patterns like listbox, tabs, and accordion involve multiple cooperating elements with specific roles and keyboard behavior — a compound component's parent can centralize that shared keyboard/focus logic once, correctly, for every sub-component that uses it.
SEO Implications
- 1
Compound Components Have No Direct SEO Effect
This is purely a component API design pattern, affecting how developers compose UI, with no bearing on server-rendered content or crawlability by itself.
Best Practices
Throw a Clear Error If a Sub-Component Is Used Outside Its Parent
Have each sub-component's context read include a check that throws a descriptive error (e.g. 'Select.Option must be used inside <Select>') if the context is null, catching misuse early instead of failing silently or with a cryptic error.
Keep the Context Value Focused on What Sub-Components Actually Need
Only include the specific state and setters that sub-components genuinely read — an overly broad context value makes the pattern harder to reason about and can cause unnecessary re-renders.
Frequent Bugs
Using <Select.Option> outside of a <Select> parent throws a cryptic 'cannot read property of null' error.
Add a guard inside the sub-component's context-reading logic that throws a clear, descriptive error if the context value is null, explaining that the sub-component must be rendered inside its parent.
Every Option re-renders whenever any single Option's hover state changes.
If hover-only state is stored in the shared context, every consumer of that context re-renders on every hover change. Move state that's genuinely local to one sub-component (like hover) into that sub-component's own local state instead of the shared context.
Real-World Examples
A Compound Accordion Component
An Accordion needs multiple independently expandable sections, each needing to know whether it's currently the open one. Building it as Accordion, Accordion.Item, and Accordion.Trigger, coordinated through a shared context tracking the currently open item's id, lets consumers freely arrange and style each section's JSX.
<Accordion>
<Accordion.Item id="faq-1">
<Accordion.Trigger>What is React?</Accordion.Trigger>
<Accordion.Panel>A JavaScript library for building UIs.</Accordion.Panel>
</Accordion.Item>
</Accordion>