Compound components let a component manage its own state and render children without explicit prop drilling.

Let’s see this in action. Imagine a Tabs component that manages which tab is currently active.

// Tabs.jsx
import React, { useState, createContext, useContext } from 'react';

const TabsContext = createContext();

export function Tabs({ children }) {
  const [activeTab, setActiveTab] = useState(0);

  const selectTab = (index) => {
    setActiveTab(index);
  };

  return (

    <TabsContext.Provider value={{ activeTab, selectTab }}>

      {children}
    </TabsContext.Provider>
  );
}

export function TabList({ children }) {
  return <div>{children}</div>;
}

export function Tab({ children, index }) {
  const { activeTab, selectTab } = useContext(TabsContext);
  const isActive = activeTab === index;

  return (

    <button onClick={() => selectTab(index)} style={{ fontWeight: isActive ? 'bold' : 'normal' }}>

      {children}
    </button>
  );
}

export function TabPanels({ children }) {
  return <div>{children}</div>;
}

export function TabPanel({ children, index }) {
  const { activeTab } = useContext(TabsContext);
  const isActive = activeTab === index;

  return isActive ? <div>{children}</div> : null;
}

// App.jsx
import React from 'react';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs';

function App() {
  return (
    <Tabs>
      <TabList>
        <Tab index={0}>First Tab</Tab>
        <Tab index={1}>Second Tab</Tab>
        <Tab index={2}>Third Tab</Tab>
      </TabList>
      <TabPanels>
        <TabPanel index={0}>Content for the first tab.</TabPanel>
        <TabPanel index={1}>Content for the second tab.</TabPanel>
        <TabPanel index={2}>Content for the third tab.</TabPanel>
      </Tabs>
    </Tabs>
  );
}

export default App;

Here, Tabs is the "manager" component. It holds the activeTab state. TabList, Tab, TabPanels, and TabPanel are its "children" components. They don’t need to know about each other directly. Instead, they communicate through a shared context (TabsContext) that Tabs provides. When you click a Tab, it calls selectTab from the context, which updates the activeTab state in Tabs. TabPanel then uses this activeTab value to decide whether to render its content.

This pattern solves the problem of prop drilling for state that is shared among a group of related components. Instead of passing state and callbacks down through multiple layers of intermediate components, you can create a "container" component that holds the state and provides it to its descendants via context. This makes the API cleaner and more declarative. You compose Tabs with its children, and the internal communication is managed.

The mental model is that of a "stateful container" and "stateless presenters" that are bound together by a shared context. The container (Tabs) manages the logic and state, while the presenters (Tab, TabPanel, etc.) are responsible for the presentation and user interaction. They signal their intent to the container (e.g., "I want to be selected") and the container orchestrates the response.

The Tab component doesn’t need to know the index of the TabPanel it corresponds to. It only needs its own index to signal to the Tabs component which tab was clicked. The Tabs component then uses the activeTab state to determine which TabPanel should be visible. This separation of concerns is key.

The most surprising thing is how little the "children" components need to know about the "parent" or even each other. A Tab component only needs to know its own index and have access to the selectTab function from the context. It doesn’t need to know what other tabs exist or what their content looks like. Similarly, a TabPanel only needs to know its own index and the currently activeTab from the context to decide if it should render. This extreme decoupling is what makes compound components so powerful.

The next concept to explore is how to handle more complex state management within compound components, perhaps involving multiple independent pieces of state.

Want structured learning?

Take the full React course →