← 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.
Syntax
markup
const ParentContext = createContext();

function Parent({ children }) {
  const [state, setState] = useState(initial);
  return (
    <ParentContext.Provider value={{ state, setState }}>
      {children}
    </ParentContext.Provider>
  );
}
Parent.Child = function Child() {
  const { state } = useContext(ParentContext);
  return <div>{state}</div>;
};

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>
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}
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.