Master Compound Components in React to build flexible, intuitive UI APIs. Learn to share implicit state and enforce structure without the mess of prop drilling.
Previously in this course, we explored how to eliminate prop drilling by leveraging composition and context. While that approach solves the data-flow problem, it doesn't always provide the cleanest developer experience for the consumer of your components.
When building complex UI primitives—like Tabs, Modals, or Select menus—you often end up with "Prop Hell," where the parent component is bloated with dozens of configuration props. Compound Components solve this by allowing the parent to coordinate state implicitly, while the user assembles the component structure themselves.
At its core, a Compound Component is a group of components that work together to form a single functional unit. The "Parent" component acts as a state container, and the "Children" act as the interface, communicating with that parent through a shared React Context.
Unlike standard components where you pass an object of settings, Compound Components rely on Inversion of Control. You provide the primitives, and the consumer decides where they go in the DOM tree. This is a foundational step toward building a Headless UI library.
Let's build a Toggle component. Instead of passing onToggle, isOn, and label to a single component, we want the API to look like this:
JSX<Toggle> <Toggle.On>The switch is on!</Toggle.On> <Toggle.Off>The switch is off!</Toggle.Off> <Toggle.Button /> </Toggle>
We need a context to share the on state and the toggle function.
JSXconst ToggleContext = React.createContext(); function Toggle({ children }) { const [on, setOn] = React.useState(false); const toggle = () => setOn(!on); // Memoize the value to avoid unnecessary re-renders const value = React.useMemo(() => ({ on, toggle }), [on]); return ( <ToggleContext.Provider value={value}> {children} </ToggleContext.Provider> ); }
We attach the sub-components to the main Toggle function object. This makes the API discoverable (e.g., Toggle.Button).
JSXfunction ToggleOn({ children }) { const { on } = React.useContext(ToggleContext); return on ? children : null; } function ToggleOff({ children }) { const { on } = React.useContext(ToggleContext); return !on ? children : null; } function ToggleButton() { const { on, toggle } = React.useContext(ToggleContext); return <button onClick={toggle}>{on ? CE9178">'ON' : CE9178">'OFF'}</button>; } // Attach sub-components Toggle.On = ToggleOn; Toggle.Off = ToggleOff; Toggle.Button = ToggleButton;
Sometimes, you need to ensure a component is used correctly. If a sub-component is used outside of its parent, it will throw an error because the context will be undefined.
Always create a custom hook to consume the context and provide a helpful error message:
JSXfunction useToggleContext() { const context = React.useContext(ToggleContext); if (!context) { throw new Error(CE9178">'Toggle sub-components must be used within <Toggle>'); } return context; }
Refactor an existing "Accordion" component in your project. Currently, it likely takes an array of objects items={[{title, content}]}.
<Accordion><Accordion.Item><Accordion.Header /><Accordion.Panel /></Accordion.Item></Accordion>.context to manage which item is currently expanded.Accordion.Header throws a meaningful error if rendered outside of an Accordion.Item.value passed to the Provider is memoized with useMemo, as we did in the example above.components/ directory if the list grows beyond 3-4 items.Compound Components provide an elegant way to handle complex UI state. By using React Context to implicitly pass state, you empower the consumer to control the markup structure while keeping your logic encapsulated and reusable. This pattern is essential for creating Design System Primitives that feel like native HTML elements.
Up next: We will look at the Render Props Pattern, which takes this concept of inversion of control even further by allowing components to share logic without dictating the UI implementation.
Master State Colocation to stop unnecessary re-renders. Learn to move state as close as possible to its consumption point for high-performance React apps.
Read moreMaster React Error Boundaries to prevent UI crashes, provide graceful fallbacks, and log production errors effectively. Build robust, stable applications today.
Designing Compound Components
Final Project Audit & Optimization
Advanced Hook Patterns
Managing Global State with Zustand/Redux
Testing Performance-Critical Components
Static Site Generation (SSG) Patterns
Internationalization (i18n) Architecture
Accessibility (a11y) in Advanced Components
Managing Third-Party Integrations
Advanced Form Handling
Using Portals for UI Overlays
Implementing Virtualized Lists
Building Design System Primitives
Managing Large-Scale Data Fetching
Micro-Frontends with React
Security Best Practices in React
Advanced Ref Usage
Memoization Pitfalls
Mastering React Patterns for Scalability
Advanced TypeScript with React