Compound components let a group of related components share implicit state through Context, giving consumers full control over layout and composition. Think of how <select> and <option> work together in HTML — neither makes sense alone, but together they form a flexible API where the browser handles state internally. This is the same idea, applied to React component libraries.
Master react compound components
Take the React Components & Patterns course with hands-on lessons and challenges.
Configuration-object APIs hit a wall fast. Every layout variation becomes another prop, and custom rendering requires render props bolted onto an already crowded interface. Compound components solve this by giving the consumer JSX instead of config.
The parent Tabs component owns the state and provides it through Context. Each sub-component reads only what it needs. Notice how the consumer can put a Badge inside a specific Tab without the component API knowing about badges at all. That flexibility is the entire point.
The Accordion manages which items are open via a Set. The allowMultiple flag determines whether opening one item closes the others. Sub-components only interact with the context they need: Trigger calls toggle, Content reads openItems. This separation means you can place Trigger and Content anywhere in the tree as long as they are inside an Accordion.
A Select component demonstrates a more complex compound pattern. The parent manages open state, selection, keyboard navigation, and an option registry. Each Option registers itself on mount so the parent knows about all available options for keyboard navigation. This is a simplified version of what libraries like Radix UI and Headless UI ship.
The Modal shares onClose through Context so the Header can render a close button without the consumer wiring it up. The Footer is optional — if you only need a confirmation message, skip it entirely. Portals ensure the modal escapes parent overflow and z-index stacking. This pattern shows compound components working beyond just state sharing — they also provide structural conventions.
Supporting both modes is essential for real-world compound components. Uncontrolled mode is convenient for static UIs. Controlled mode is necessary when the active state needs to sync with URL parameters, form state, or other components. The pattern checks whether a controlled value is provided and delegates accordingly.
Using `React.Children.map` and `cloneElement` to inject props into children instead of using Context
Context works regardless of nesting depth and does not break when consumers wrap sub-components in divs, fragments, or custom wrappers. cloneElement only works on direct children and silently fails when the tree structure changes. Context is the correct primitive for compound components.
Failing to throw an error when a sub-component is used outside its parent provider — returning null or using a default value instead
Always throw a descriptive error in the custom hook: `throw new Error('Tabs.Tab must be used within <Tabs>')`. Silent failures lead to hours of debugging. The developer needs to know immediately that they placed a component in the wrong part of the tree.
Putting too much into the Context value — exposing internal state, dispatch functions, and derived values that sub-components never use
Keep the context value minimal. If some sub-components need data that others do not, consider splitting into two contexts (one for state, one for dispatch) or providing only what the public sub-components actually read. A bloated context causes unnecessary re-renders across all consumers.
Creating a new context value object on every render, which triggers re-renders in all consumers even when the actual state has not changed
Memoize the context value with `useMemo`: `const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab])`. This ensures consumers only re-render when the state they depend on actually changes.
Compound components are a group of related React components that share implicit state through Context, giving consumers full control over markup and layout. The parent component provides state via a Context Provider, and sub-components consume it through a custom hook. This pattern replaces sprawling props-based APIs with a composable, declarative interface. Attach sub-components using dot notation for discoverability, throw clear errors when components are used outside their parent, memoize the context value, and support both controlled and uncontrolled modes. Every serious React component library — Radix, Headless UI, Ark UI — builds on this pattern.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.