Chapter 2 of 4
Compound Components
Components that work together and share state implicitly.
A compound component is a family of components that only make sense together: Tabs with TabList, Tab and TabPanel. The parent holds the state and shares it with its children through context.
const TabsContext = createContext(null);
function Tabs({ defaultTab, children }) {
const [active, setActive] = useState(defaultTab);
const value = useMemo(() => ({ active, setActive }), [active]);
return (
<TabsContext.Provider value={value}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ id, children }) {
const { active, setActive } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={active === id}
onClick={() => setActive(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }) {
const { active } = useContext(TabsContext);
return active === id ? <div role="tabpanel">{children}</div> : null;
}
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;<Tabs defaultTab="overview">
<Tabs.Tab id="overview">Overview</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
<Tabs.Panel id="overview"><Overview /></Tabs.Panel>
<Tabs.Panel id="settings"><Settings /></Tabs.Panel>
</Tabs>The user of the component decides layout, wrappers and ordering, while the shared state stays hidden. This is how most headless UI libraries are built.
The older cloneElement approach
Before context was convenient, compound components injected props by cloning their children. You still meet this in older code.
function Tabs({ children }) {
const [active, setActive] = useState(0);
return Children.map(children, (child, index) =>
cloneElement(child, { isActive: index === active, onSelect: () => setActive(index) })
);
}