← Back to React Course | Chapter 6: Custom Hooks & Advanced Patterns | Lesson 9 of 10

Compound Component Pattern

Compound components are like a TV remote and TV that are sold separately but designed to work together perfectly as one system.

What Makes Components 'Compound'

Compound components are multiple components meant to be used together as a set, like HTML's <select> and <option>. Instead of one component accepting a huge configuration prop, the API is broken into smaller pieces that communicate implicitly through shared context, giving the caller more layout freedom.

Note: A good sign you need compound components: a single component's prop list is growing long and awkward (items, renderItem, itemClassName, ...).

Warning: Compound components only work correctly when the pieces are rendered inside their intended parent — using Tabs.Tab outside <Tabs> has no shared state to connect to.

Example: What Makes Components 'Compound'

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
const TabsContext = React.createContext();
function Tabs({ children, defaultTab }) {
  const [active, setActive] = React.useState(defaultTab);
  return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
function Tab({ id, children }) {
  const { active, setActive } = React.useContext(TabsContext);
  return <button onClick={() => setActive(id)} style={{fontWeight: active === id ? "bold" : "normal"}}>{children}</button>;
}
function App() {
  return <Tabs defaultTab="a"><Tab id="a">Tab A</Tab><Tab id="b">Tab B</Tab></Tabs>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Sharing State via Context Instead of Props

Rather than passing the active tab down through explicit props at every level, compound components store shared state in a Context provided by the parent. Any sub-component can then read that context directly, no matter how deeply it's nested inside the parent's children.

Note: Create the Context object outside the component definitions (at module scope) so it isn't recreated on every render.

Warning: If you forget to check for a missing context (e.g. Tab used without a Tabs parent), useContext returns undefined and destructuring it throws a confusing error.

Example: Sharing State via Context Instead of Props

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
const PanelContext = React.createContext();
function Accordion({ children }) {
  const [openIndex, setOpenIndex] = React.useState(null);
  return <PanelContext.Provider value={{ openIndex, setOpenIndex }}>{children}</PanelContext.Provider>;
}
function Panel({ index, title, children }) {
  const { openIndex, setOpenIndex } = React.useContext(PanelContext);
  const isOpen = openIndex === index;
  return <div><button onClick={() => setOpenIndex(isOpen ? null : index)}>{title}</button>{isOpen && <p>{children}</p>}</div>;
}
function App() {
  return <Accordion><Panel index={0} title="Section 1">Content 1</Panel><Panel index={1} title="Section 2">Content 2</Panel></Accordion>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Attaching Sub-Components for a Clean API

A common finishing touch is attaching the sub-components as properties of the main component, like Tabs.Tab = Tab. This lets consumers import just one name (Tabs) and access everything through it, making the related pieces obviously connected.

Note: This is purely an ergonomic/discoverability improvement — Tabs.Tab and a separately-exported Tab function behave identically at runtime.

Warning: Attaching sub-components after defining them (Tabs.Tab = Tab) must happen after both Tabs and Tab are declared, or you'll assign undefined.

Example: Attaching Sub-Components for a Clean API

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
const TabsContext = React.createContext();
function Tabs({ children, defaultTab }) {
  const [active, setActive] = React.useState(defaultTab);
  return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
function Tab({ id, children }) {
  const { active, setActive } = React.useContext(TabsContext);
  return <button onClick={() => setActive(id)} style={{fontWeight: active === id ? "bold" : "normal"}}>{children}</button>;
}
Tabs.Tab = Tab;
function App() {
  return <Tabs defaultTab="x"><Tabs.Tab id="x">X</Tabs.Tab><Tabs.Tab id="y">Y</Tabs.Tab></Tabs>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Using compound components without setting up shared state via Context, forcing awkward prop-drilling between the pieces instead.
  2. Not exporting the sub-components as properties of the parent (like Tabs.Panel), making the API less discoverable.
  3. Allowing the sub-components to be used outside their parent, where they have no shared context and silently fail.
Chapter Summary
  • Compound components are a set of components designed to work together, sharing implicit state via Context.
  • The classic example is a Tabs component made of Tabs, Tabs.List, Tabs.Tab, and Tabs.Panel working together.
  • This pattern gives the consumer flexible control over layout while the parent manages the shared state internally.
  • It's commonly implemented using React Context to pass state down without manual prop drilling.
Browser Support

Available since React 16.3 (stable Context API) — earlier versions could approximate this with React.Children/cloneElement.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.