What is the Compound Components Pattern?
Learn the React compound components pattern: share state via context, build flexible Tabs-style APIs, and compose related components without prop drilling.
Expected Interview Answer
The compound components pattern is a React design where several related components work together as one cohesive unit, sharing implicit state through context so the consumer composes them freely without passing props between them.
A parent component (for example Tabs) owns the state and exposes it via React Context. Child components (Tabs.List, Tabs.Tab, Tabs.Panel) read that context to coordinate, so the user writes expressive JSX like a small custom language. This inverts control: the parent manages logic while the consumer decides structure, layout and ordering, keeping the API flexible without a huge prop list.
- Flexible, declarative markup with no prop drilling
- Encapsulates shared state inside the parent
- Consumers control layout and composition
- Cleaner API than one component with dozens of props
- Easy to extend with new sub-components
AI Mentor Explanation
A cricket team is a compound unit: the captain holds the game plan and signals field changes, while bowlers, fielders and the wicketkeeper each read those signals and act in sync. No player passes instructions hand to hand; they all tune into the captain's shared strategy. Compound components work the same way, with the parent holding state in context and each child reacting to it.
Step-by-Step Explanation
Step 1
Create the parent and context
Build a parent component that owns state with useState or useReducer and creates a React Context to expose it.
Step 2
Provide the value
Wrap the parent's children in the context Provider, passing the shared state and updater functions as the value.
Step 3
Build sub-components
Create child components (List, Item, Panel) that call useContext to read the shared state and behave accordingly.
Step 4
Attach children to the parent
Expose sub-components as static properties (Tabs.List = TabsList) so the API reads as one cohesive namespace.
Step 5
Guard against misuse
Throw a clear error if a sub-component is used outside its parent, so the missing context fails loudly.
What Interviewer Expects
- Understanding of React Context for implicit state sharing
- Why this avoids prop drilling and prop explosion
- How static properties create a cohesive namespace API
- Trade-offs versus render props or configuration props
- A concrete example like a Tabs or Accordion component
Common Mistakes
- Passing all state as explicit props instead of using context
- Forgetting to throw when a child renders outside its parent
- Overusing the pattern for simple components that need no coordination
- Recreating the context value object every render, causing extra re-renders
Best Answer (HR Friendly)
“Compound components let you build a set of React pieces that automatically work together, like a Tabs component with its tabs and panels. The parent quietly shares the needed information so you can arrange the pieces however you like without wiring them up by hand.”
Code Example
import { createContext, useContext, useState } from 'react'
const TabsContext = createContext(null)
function Tabs({ children, defaultValue }) {
const [active, setActive] = useState(defaultValue)
return (
<TabsContext.Provider value={{ active, setActive }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
function useTabs() {
const ctx = useContext(TabsContext)
if (!ctx) throw new Error('Tabs.* must be used inside <Tabs>')
return ctx
}
function TabList({ children }) {
return <div role="tablist">{children}</div>
}
function Tab({ value, children }) {
const { active, setActive } = useTabs()
return (
<button role="tab" aria-selected={active === value} onClick={() => setActive(value)}>
{children}
</button>
)
}
function Panel({ value, children }) {
const { active } = useTabs()
return active === value ? <div role="tabpanel">{children}</div> : null
}
Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panel = Panel
export default function Example() {
return (
<Tabs defaultValue="a">
<Tabs.List>
<Tabs.Tab value="a">First</Tabs.Tab>
<Tabs.Tab value="b">Second</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="a">Panel A</Tabs.Panel>
<Tabs.Panel value="b">Panel B</Tabs.Panel>
</Tabs>
)
}Follow-up Questions
- How would you memoize the context value to avoid unnecessary re-renders?
- How does the compound components pattern compare to the render props pattern?
- How would you add keyboard navigation and ARIA roles to the tabs?
- When would a controlled compound component be preferable to an uncontrolled one?
- How do you type a compound component with static properties in TypeScript?
MCQ Practice
1. What mechanism do compound components typically use to share state implicitly?
The parent creates a Context and provides shared state; sub-components consume it, avoiding prop drilling.
2. Why are sub-components often attached as static properties (Tabs.List)?
Static properties group related components under one name, making the API expressive and easy to discover.
3. What is a good practice when a sub-component is used outside its parent?
Throwing when context is missing fails loudly and helps developers spot incorrect usage immediately.
Flash Cards
What is the compound components pattern? — Related components that share implicit state via context and compose as one cohesive unit.
How is state shared between compound components? — The parent owns state and exposes it through React Context; children consume it with useContext.
Why attach children as static properties? — It creates a discoverable namespace (Tabs.List, Tabs.Tab) and a clean, expressive API.
One downside of the pattern? — Recreating the context value each render can trigger unnecessary re-renders unless memoized.