React Compound Components

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 Tabs component 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.

  • Tabs holds activeIndex, the currently selected tab, in state.
  • It shares activeIndex and setActiveIndex through TabsContext.
  • {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.

  • Tab reads the shared state, knows if it’s active, and sets itself active on click.
  • TabPanel reads activeIndex and 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 Tab updates the shared state, and the matching TabPanel shows.

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, and TabPanel.

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. 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. 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. 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. 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.

React Higher Order Components

Share & Connect