Compound components
Parts share state via a parent Context, with no prop drilling.
From frontend-patterns, section “Compound Components”.
Who carries the active tab
Click a tab, then raise the depth.
Prop drillingBad
12 props
ContextGood
0 props
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
export function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
)
}
A Tab outside Tabs
Drag Billing out by its handle, then click it.
Props, no guardBad
Context with a guardGood
export function Tab({ id, children }) {
const context = useContext(TabsContext)
if (!context) throw new Error('Tab must be used within Tabs')
return <button onClick={() => context.setActiveTab(id)}>{children}</button>
}
Add a tab
Type an id, press Enter, compare the JSX.
Prop drillingBad
8 props
Compound componentsGood
0 props
Why it matters, pros and cons
Why it matters
Parts share state via context. A Tab used alone throws instead of failing quietly.
Pros
- New layers or tabs add no wiring.
- Misuse throws on first render.
- Call site reads like the markup.
Cons
- Provider and readers need
"use client". - Every reader re-renders when the value changes.
- One parent, one child: plain props are simpler.