Skip to main content

What does the "compound components" pattern do with context?

What "Compound Components" are

Compound Components (composite components) are a pattern where several related components work together as a single "component group".

Usually we have a "parent" and "child elements" that communicate with each other through React Context instead of passing props manually.


Example (intuitively)

Without the pattern

javascript
<Tabs> <TabList> <Tab label="Description" activeTab={activeTab} setActiveTab={setActiveTab} /> <Tab label="Reviews" activeTab={activeTab} setActiveTab={setActiveTab} /> </TabList> <TabPanels activeTab={activeTab}> <TabPanel index={0}>Content 1</TabPanel> <TabPanel index={1}>Content 2</TabPanel> </TabPanels> </Tabs>

Downsides:

  • you have to thread a bunch of props (activeTab, setActiveTab);
  • the components are "tightly coupled" - using them separately is inconvenient.

With the Compound Components pattern

Goal:

  • For Tabs, TabList, Tab, TabPanel to know their own state on their own through an internal context;
  • for the code to look natural:
javascript
<Tabs> <Tabs.List> <Tabs.Tab>Description</Tabs.Tab> <Tabs.Tab>Reviews</Tabs.Tab> </Tabs.List> <Tabs.Panels> <Tabs.Panel>Content 1</Tabs.Panel> <Tabs.Panel>Content 2</Tabs.Panel> </Tabs.Panels> </Tabs>

How it works

1. Create the context

javascript
const TabsContext = createContext(null);

2. The parent component (Provider)

javascript
function Tabs({ children }) { const [activeIndex, setActiveIndex] = useState(0); const value = useMemo(() => ({ activeIndex, setActiveIndex }), [activeIndex]); return ( <TabsContext.Provider value={value}> <div className="tabs">{children}</div> </TabsContext.Provider> ); }

3. Child components - context subscribers

javascript
function TabList({ children }) { return <div className="tab-list">{children}</div>; } function Tab({ children, index }) { const { activeIndex, setActiveIndex } = useContext(TabsContext); const isActive = index === activeIndex; return ( <button className={isActive ? "active" : ""} onClick={() => setActiveIndex(index)} > {children} </button> ); } function Panels({ children }) { return <div className="tab-panels">{children}</div>; } function Panel({ children, index }) { const { activeIndex } = useContext(TabsContext); return activeIndex === index ? <div>{children}</div> : null; }

4. Assembling the "compound" component

javascript
Tabs.List = TabList; Tabs.Tab = Tab; Tabs.Panels = Panels; Tabs.Panel = Panel;

What context does here

Context is used as an internal linking mechanism between components, so they can "know" the parent's (Tabs) state without requiring props at every level.

That is:

  • Tabs manages the state (the active tab);
  • Tabs.Tab and Tabs.Panel are simply "subscribed" to that context;
  • everything synchronizes automatically.

Advantages of the pattern

AdvantageDescription
Clean APINested JSX reflects the logic's hierarchy
ReusabilityComponents can be reused independently
EncapsulationThe Tabs state is hidden, accessible only through context
FlexibilityYou can insert your own components between them
No prop drillingNo props are passed down manually

Where this is used

This pattern is used everywhere:

  • in Headless UI, Radix UI, shadcn/ui, React Aria;
  • for Tabs, Accordion, Dropdown, Menu, Modal, Form, Stepper.

Summary

Compound Components + Context =

a flexible interface where child components automatically know which context they are in, and synchronize without props.


The pattern's formula:

  1. Create a context inside the "parent";
  2. Provide the value through <Provider>;
  3. Child components pull the state from the context;
  4. Export the child components as properties of the main one.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.