React

React Compound Components👨‍💻

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.

Key Takeaways

  • 1Compound components share state implicitly through React Context — child components read from the parent provider without any prop drilling
  • 2The dot notation API (`Tabs.Tab`, `Tabs.Panel`) groups sub-components under the parent, making the API discoverable and signaling that these components belong together
  • 3This pattern gives consumers total control over the markup: they can reorder children, wrap them in custom layouts, or conditionally render specific parts without the component library needing to anticipate every case
  • 4Always throw a descriptive error when a sub-component is used outside its parent provider — silent failures waste hours of debugging time
  • 5Supporting both controlled and uncontrolled modes makes compound components drop-in for simple use cases and fully integrable in complex state management scenarios
  • 6Every major headless UI library (Radix, Headless UI, Ark UI) uses compound components as their primary API pattern — this is not a niche technique, it is the industry standard for component library design

Master react compound components

Take the React Components & Patterns course with hands-on lessons and challenges.

Examples

The problem: a props-heavy component that falls apart

tsx

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.

Tabs — the classic compound component

tsx

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.

Accordion with single and multi-expand modes

tsx

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.

Select/Dropdown with keyboard navigation

tsx

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.

Modal with Header, Body, Footer slots

tsx

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.

Controlled mode — letting parents own the state

tsx

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.

Common Mistakes

Mistake:

Using `React.Children.map` and `cloneElement` to inject props into children instead of using Context

Fix:

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.

Mistake:

Failing to throw an error when a sub-component is used outside its parent provider — returning null or using a default value instead

Fix:

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.

Mistake:

Putting too much into the Context value — exposing internal state, dispatch functions, and derived values that sub-components never use

Fix:

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.

Mistake:

Creating a new context value object on every render, which triggers re-renders in all consumers even when the actual state has not changed

Fix:

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.

Best Practices

  • Use the dot notation pattern (`Tabs.Tab`, `Tabs.Panel`) to attach sub-components to the parent. This makes the API self-documenting — autocomplete shows all available sub-components when you type `Tabs.`
  • Support both controlled and uncontrolled modes. Accept `defaultValue` for uncontrolled usage and `value` + `onChange` for controlled usage. Check if the controlled prop is defined to determine which mode to use.
  • Memoize the Context value with `useMemo` to prevent unnecessary re-renders. This matters especially when the parent component has props or state unrelated to the compound component's shared state.
  • Write a custom hook (e.g., `useTabs`, `useAccordion`) that throws on null context instead of using `useContext` directly in sub-components. This centralizes error handling and gives you a single place to add logging or validation.
  • Keep sub-components focused on one responsibility. The Trigger handles click events, the Content handles visibility, the Item provides grouping. Do not merge responsibilities — it defeats the composability that makes this pattern valuable.

Summary

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.

Practice React with hands-on challenges

Learn react compound components hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master React with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.