React Compound Components
Table of Contents + −
In the last lesson, React Performance Best Practices, you wrapped up the performance module. Now we start advanced patterns with compound components, a set of components that work together as one feature and share state behind the scenes.
🤔 Why do we need compound components?
Here’s the pain. A component that takes everything through props gets ugly fast as it grows.
- A
Tabscomponent configured by props might look like<Tabs labels={[...]} contents={[...]} />. - Two parallel arrays that must line up are easy to get wrong and hard to read.
- Adding a feature, like an icon per tab, means adding yet another prop array.
- The user of your component has no control over the layout.
So compound components solve this. Instead of one big prop-stuffed component, you give a set of components that the user arranges themselves, while they quietly share state together.
🐍 What is the compound components pattern?
Let’s define it simply.
- A compound component is a group of components designed to work together as one feature.
- A parent component holds the shared state.
- Child components read and use that shared state, without the user wiring it up.
- The user composes them in JSX, like
<Tabs>with<Tab>children, and it just works.
Think of how <select> and <option> work in HTML. You write the options inside the select, and they cooperate automatically. Compound components bring that feel to your own React components.
🧩 How they share state: context
The trick that makes the children cooperate is context. This is the Context API you already learned, used inside one feature.
- The parent creates a context and provides the shared state, like which tab is active.
- Each child reads that context to know how to behave.
- So the children don’t need props from the user to coordinate; they coordinate through the shared context.
So compound components are really “a feature-sized context”: the parent shares state, the children consume it, all hidden from the person using the component.
💡 Building a Tabs compound component
Let’s build a Tabs set: Tabs holds which tab is active, TabList and Tab switch tabs, and TabPanel shows the active content. First, the context and the parent.
import { createContext, useContext, useState } from "react";
const TabsContext = createContext();
function Tabs({ children }) { const [activeIndex, setActiveIndex] = useState(0); const value = { activeIndex, setActiveIndex }; return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;}Read the parent.
TabsholdsactiveIndex, the currently selected tab, in state.- It shares
activeIndexandsetActiveIndexthroughTabsContext. {children}lets the user put whatever tabs and panels they want inside.
Now the children, which read the context to cooperate.
function Tab({ index, children }) { const { activeIndex, setActiveIndex } = useContext(TabsContext); const isActive = index === activeIndex;
return ( <button onClick={() => setActiveIndex(index)} style={{ fontWeight: isActive ? "bold" : "normal" }} > {children} </button> );}
function TabPanel({ index, children }) { const { activeIndex } = useContext(TabsContext); // only show this panel if its index is the active one return index === activeIndex ? <div>{children}</div> : null;}Walk through the children.
Tabreads the shared state, knows if it’s active, and sets itself active on click.TabPanelreadsactiveIndexand shows its content only when it’s the active one.- Neither child needed the user to wire up the shared state; they get it from context.
🖥️ Using the compound component
Now the payoff. The person using Tabs composes the pieces naturally, and arranges the layout however they like.
function App() { return ( <Tabs> <Tab index={0}>Home</Tab> <Tab index={1}>Profile</Tab>
<TabPanel index={0}>Welcome home!</TabPanel> <TabPanel index={1}>This is your profile.</TabPanel> </Tabs> );}See how clean the usage is.
- The user writes the tabs and panels right where they want them.
- No parallel arrays, no giant config prop, just readable nested components.
- Clicking a
Tabupdates the shared state, and the matchingTabPanelshows.
Output
[Home] [Profile]Welcome home!
(click Profile)[Home] [Profile]This is your profile.So the components cooperate through context, while the user just composes them. That’s the whole pattern.
⚠️ Common Mistakes
A common mistake is having the children read context without a parent providing it, so they have nothing to share.
// ❌ a Tab used outside Tabs - no provider, so useContext gives undefined and it crashes<Tab index={0}>Lonely tab</Tab>
// ✅ children must be inside the parent that provides the context<Tabs> <Tab index={0}>Home</Tab></Tabs>Keep these in mind.
- Don’t use the child components outside their parent. They need the parent’s context to work.
- Don’t pass the shared state manually as props; let the children read it from context.
- Don’t overuse this pattern. It’s for sets of components that genuinely belong together, like tabs, accordions, or menus.
✅ Best Practices
A few habits for compound components.
- Use the pattern for features made of parts that belong together, like Tabs, Accordion, or Menu.
- Hold the shared state in the parent and pass it through context, not as props from the user.
- Keep each child focused on its one job (a tab button, a panel) and reading what it needs from context.
- Give clear names so the relationship is obvious, like
Tabs,Tab, andTabPanel.
You've seen this in libraries
Many UI libraries use this exact pattern, like <Menu> with <MenuItem>, or <Accordion> with <AccordionItem>. Once you recognize it, you’ll see compound components everywhere, and now you can build your own.
🧩 What You’ve Learned
- ✅ Compound components are a set of components that work together as one feature
- ✅ They give a flexible API where the user composes the parts in JSX, like
<Tabs>with<Tab>children - ✅ The parent holds the shared state and provides it through context
- ✅ The children read that context to cooperate, with no manual wiring from the user
- ✅ It replaces ugly config-prop or parallel-array APIs with readable nested components
- ✅ Use it for parts that genuinely belong together, like tabs, accordions, and menus
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What is the compound components pattern?
Why: Compound components are a group of related components (like Tabs and Tab) that cooperate as one feature, with the user composing them in JSX.
- 2
How do the child components in this pattern share state?
Why: The parent holds the shared state and provides it via context. Each child reads that context to coordinate, so the user doesn't have to wire it up.
- 3
What problem does this pattern solve compared to a prop-heavy component?
Why: Instead of stuffing everything into props or lining up parallel arrays, the user composes clear nested components, which is far more readable and flexible.
- 4
What happens if you use a child component outside its parent?
Why: The children depend on the parent's context. Used outside the parent, there's no provider, so useContext returns nothing and the child can't work.
🚀 What’s Next?
Now you can build flexible sets of components that cooperate through context. Next we’ll look at an older but still useful pattern for reusing logic across components: higher order components.