Compound components are a pattern in React where multiple components work together to form a cohesive UI while maintaining a shared implicit state. This pattern is particularly useful for building reusable, flexible components like tabs, accordions, or forms.
How Compound Components Work
The key idea is that parent and child components share state and behavior implicitly, without the need for explicit prop drilling. This creates a more intuitive API for consumers of your component.
Basic Example
import { useState } from 'react';
function Tabs({ children }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div className="tabs">
{children.map((child, index) => (
React.cloneElement(child, {
isActive: index === activeTab,
onSelect: () => setActiveTab(index),
key: index
})
))}
</div>
);
}
function Tab({ isActive, onSelect, children }) {
return (
<button
onClick={onSelect}
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{children}
</button>
);
}
// Usage
function App() {
return (
<Tabs>
<Tab>First Tab</Tab>
<Tab>Second Tab</Tab>
<Tab>Third Tab</Tab>
</Tabs>
);
}
Advantages of Compound Components
- Better Separation of Concerns: Each component handles its own part of the UI
- More Flexible API: Consumers can rearrange or customize parts of the component
- Implicit State Sharing: State is managed internally without requiring consumers to handle it
- Cleaner JSX: Results in more readable component composition
Implementing with Context API
For more complex cases, React’s Context API is often used with compound components:
import { createContext, useContext, useState } from 'react';
const TabsContext = createContext();
function Tabs({ children }) {
const [activeTab, setActiveTab] = useState(0);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ index, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
onClick={() => setActiveTab(index)}
style={{ fontWeight: index === activeTab ? 'bold' : 'normal' }}
>
{children}
</button>
);
}
function TabPanel({ index, children }) {
const { activeTab } = useContext(TabsContext);
return activeTab === index ? <div>{children}</div> : null;
}
// Usage
function App() {
return (
<Tabs>
<div className="tab-list">
<Tab index={0}>First</Tab>
<Tab index={1}>Second</Tab>
</div>
<TabPanel index={0}>First content</TabPanel>
<TabPanel index={1}>Second content</TabPanel>
</Tabs>
);
}
Real-world Examples
Popular libraries use compound components:
- React Router’s
<BrowserRouter>
,<Route>
,<Link>
- Reach UI’s
<Menu>
,<MenuButton>
,<MenuList>
,<MenuItem>
- Downshift’s autocomplete components
Best Practices
- Use
React.Children.map
andReact.cloneElement
for simple cases - Use Context API for more complex state sharing
- Provide sensible defaults where possible
- Document the expected structure of child components
- Consider TypeScript for better type safety with compound components
Compound components are a powerful pattern that can make your React components more flexible and maintainable when you need to create complex, reusable UI elements.