Compound Components Pattern
Compound components are a pattern where several related components work together as one cohesive unit, sharing state implicitly through a common context instead of through props passed at every level — like <Tabs> with its <Tab> and <TabPanel> children. The result is a flexible, declarative API for the people who use your component.
Learn Compound Components Pattern in our free React course — an interactive lesson with runnable examples, a practice exercise and a quick recall.
Part of the free React course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
1️⃣ The Shared State Is the Whole Idea
A compound component centers on one piece of shared state — for tabs, "which tab is active." Each sub-component reads that state to render itself and calls a setter to change it. Crucially, the children don't receive this through props; they reach into a shared context, so consumers can nest and arrange them freely.
2️⃣ Build It With a Context Provider
The parent <Tabs> owns the state and wraps its children in a Provider. Sub-components call useContext to read the active id and the setter. No props are threaded through intermediate elements — that's the "implicit" sharing.
3️⃣ The Panel and Misuse Guards
A <TabPanel> renders its children only when its id matches the active tab. To keep the API safe, default the context to null and throw a clear error if a sub-component is used outside <Tabs> , so mistakes fail loudly.
🧩 Reorder Challenge
These lines build the Tabs parent that provides shared state. Put them in the right order:
Common Errors (and the fix)
- "Cannot read properties of null". — A sub-component was rendered outside the parent Provider. Add a useTabs guard that throws a clear message.
- New value object every render. — All consumers re-render unnecessarily. Memoize the context value with useMemo .
- Drilling props anyway. — If you're passing active down by hand, you've skipped the point. Read it from context in each sub-component.
- One giant component with config props. — Rigid and unreadable. Split into cooperating sub-components sharing context.
📋 Quick Reference
Piece
Role
createContext(null)
Shared channel for sub-components
<Ctx.Provider value={x}>
Parent supplies the shared state
useContext(Ctx)
Sub-component reads the state
throw if null
Guard against misuse outside parent
🧠 Quick Recall
1. How do sub-components share state without prop drilling?
Through a context: the parent renders a Provider, and each sub-component reads the value with useContext .
2. What's the benefit over one big config-prop component?
A flexible, declarative API: consumers arrange the sub-components freely and React wires up the shared behavior.
3. How do you stop a sub-component being used outside the parent?
Default the context to null and throw a clear error in the consumer hook when it's null.
Frequently Asked Questions
Mini-Challenge: A Tiny Accordion
Model an accordion where only one panel is open, sharing the open id through a context object the panels read. Run it and check your output.
🎉 Lesson Complete
- ✅ Compound components cooperate as one unit via shared state
- ✅ State is shared implicitly through context , not prop drilling
- ✅ The parent renders a Provider ; sub-components useContext
- ✅ Guard with a null context + thrown error for misuse
- ✅ Next lesson: Render Props — share logic with a function-as-children
Practice quiz
What defines a set of 'compound components'?
- Components that all live in one file
- Components that take no props
- Separate components designed to work together as one unit, sharing state implicitly
- Components rendered with portals
Answer: Separate components designed to work together as one unit, sharing state implicitly. Like <Tabs>, <Tab>, <TabPanel>, each is its own component but they cooperate through shared state.
How do compound sub-components share state without prop drilling?
- The parent provides a context and each sub-component reads it with useContext
- Through global variables
- By passing props through every level
- Through localStorage
Answer: The parent provides a context and each sub-component reads it with useContext. The parent renders a context Provider; sub-components call useContext to read or update the shared state.
Where does the shared state (e.g. which tab is active) live?
- In each individual sub-component
- In a separate Redux store only
- In the DOM
- In the parent component that renders the Provider
Answer: In the parent component that renders the Provider. The parent owns the state with useState and supplies it via the Provider's value.
What goes on the context's Provider so children can read it?
- A children prop
- A value prop holding the shared state
- A ref prop
- A key prop
Answer: A value prop holding the shared state. <Ctx.Provider value={{ active, setActive }}> supplies the shared state to all consumers below it.
How do you stop a sub-component being used outside its parent?
- Default the context to null and throw a clear error if useContext returns null
- Wrap it in try/catch
- Use a default prop
- Disable the component in production
Answer: Default the context to null and throw a clear error if useContext returns null. A guard hook throws a helpful error when context is null, so <Tab> used outside <Tabs> fails loudly.
Why is the compound pattern better than one big config-prop component?
- It runs without React
- It avoids using context
- It gives consumers a flexible, declarative API they arrange freely
- It removes the need for keys
Answer: It gives consumers a flexible, declarative API they arrange freely. Consumers nest sub-components however they like and React wires up the shared behavior — far more flexible.
What does a <TabPanel> typically do?
- Always render its children
- Render its children only when its id matches the active tab
- Set the active tab on mount
- Render the parent again
Answer: Render its children only when its id matches the active tab. TabPanel reads the active id from context and renders its children only when active === id, else null.
A new value object is created on every parent render. What problem can that cause?
- The app crashes
- The context becomes null
- Children lose their keys
- All context consumers re-render unnecessarily
Answer: All context consumers re-render unnecessarily. A fresh value each render makes every consumer re-render; memoize the value with useMemo to avoid it.
Which hook does a sub-component call to read the shared state?
- useState
- useContext
- useRef
- useReducer
Answer: useContext. Sub-components call useContext(TabsCtx) to read the active id and setter the parent provided.
Where should createContext(null) be called for a compound component?
- Inside each sub-component's render
- Inside an effect
- Once at module scope so all sub-components share the same context
- On every click
Answer: Once at module scope so all sub-components share the same context. Create the context once at module scope so every sub-component references the same shared channel.
Continue this course
- Previous: forwardRef & useImperativeHandle
- Next: Render Props Pattern