DEV Community

Cover image for React Compound Components: The "Build Your Own Adventure" Pattern
Masudur Rahman Sourav
Masudur Rahman Sourav

Posted on

React Compound Components: The "Build Your Own Adventure" Pattern

If you've ever bought a piece of furniture from a certain Swedish store, you know the drill. You don't buy a pre-assembled "Sitting Unit." You buy a frame, cushions, legs, and maybe a weird little wrench, and you put it together yourself.
Compound Components are the Ikea furniture of React.

Instead of building one giant, monolithic component that takes 500 props (), you build a set of smaller components that work together like magic. They share state behind the scenes, but you decide where everything goes.

Let's dive into the pattern that makes libraries like Radix UI and Headless UI possible.

🎨 The Graphical Explanation

Imagine you are building a Tabs component.

The "Mega-Prop" Way (The Old Way) πŸ¦–

You pass a giant configuration object. The component decides the layout. You have zero control. If you want to move the "Tab List" below the "Panels," you have to rewrite the library.


       +-----------------------------+
       |      <Tabs config={...} />  |
       +-----------------------------+
       | [Tab 1] [Tab 2] [Tab 3]     |  <-- Hardcoded position
       |                             |
       |  Content for Tab 1...       |
       +-----------------------------+

Enter fullscreen mode Exit fullscreen mode

The Compound Component Way (The Flexible Way) 🀸

You export a parent (Tabs) and several children (Tab, Panel). The Parent holds the state (which tab is active), but the User decides the layout.

       <Tabs>
          <TabList>  <-- You put this where YOU want
             <Tab>One</Tab>
             <Tab>Two</Tab>
          </TabList>

          <TabPanels>
             <Panel>Content 1</Panel>
             <Panel>Content 2</Panel>
          </TabPanels>
       </Tabs>
Enter fullscreen mode Exit fullscreen mode

The Parent (Tabs) secretly passes state to the children using Context. They communicate telepathically. 🧠✨

πŸ’» The Code: From "Prop Hell" to "Component Heaven"

Let's build a simple Accordion (toggle sections).

1. The Setup (Creating the Context)

First, we need a secret channel for our components to talk.

import React, { createContext, useContext, useState } from 'react';

// 1. Create the Context
const AccordionContext = createContext();

// 2. Create the Parent Component
const Accordion = ({ children }) => {
  const [openIndex, setOpenIndex] = useState(0);

  // The toggle function logic
  const toggleIndex = (index) => {
    setOpenIndex(prev => (prev === index ? null : index));
  };

  return (
    <AccordionContext.Provider value={{ openIndex, toggleIndex }}>
      <div className="accordion-wrapper">
        {children}
      </div>
    </AccordionContext.Provider>
  );
};

Enter fullscreen mode Exit fullscreen mode

2. The Children (Consuming the Context)

Now we create the sub-components. Notice they don't accept isOpen or onClick as props from the user. They grab it from the Context!

// 3. The Item Component (Just a wrapper, usually)
const AccordionItem = ({ children }) => {
  return <div className="accordion-item">{children}</div>;
};

// 4. The Trigger (The clickable part)
const AccordionHeader = ({ children, index }) => {
  const { toggleIndex, openIndex } = useContext(AccordionContext);
  const isActive = openIndex === index;

  return (
    <button 
      className={`accordion-header ${isActive ? 'active' : ''}`}
      onClick={() => toggleIndex(index)}
    >
      {children} {isActive ? 'πŸ”½' : '▢️'}
    </button>
  );
};

// 5. The Content (The hidden part)
const AccordionPanel = ({ children, index }) => {
  const { openIndex } = useContext(AccordionContext);

  if (openIndex !== index) return null;

  return <div className="accordion-panel">{children}</div>;
};

// Attach sub-components to the Parent for cleaner imports (Optional but cool)
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

export default Accordion;

Enter fullscreen mode Exit fullscreen mode

3. Usage (The Magic Moment) πŸͺ„

Look how clean this API is. No complex config objects. Just JSX.

import Accordion from './Accordion';

const FAQ = () => (
  <Accordion>

    <Accordion.Item>
      <Accordion.Header index={0}>Is React hard?</Accordion.Header>
      <Accordion.Panel index={0}>
        Only until you understand `useEffect`. Then it's just chaotic.
      </Accordion.Panel>
    </Accordion.Item>

    <Accordion.Item>
      <Accordion.Header index={1}>Why use Compound Components?</Accordion.Header>
      <Accordion.Panel index={1}>
        Because passing 50 props is bad for your blood pressure.
      </Accordion.Panel>
    </Accordion.Item>

  </Accordion>
);

Enter fullscreen mode Exit fullscreen mode

πŸ† Why Should You Do This? (Use Cases)

1. UI Libraries (Tabs, Selects, Menus)

If you are building a design system, this is mandatory. Users will want to put an icon inside the Tab, or move the Label below the Input. Compound components let them do that without bugging you to add a renderLabelBottom prop.

2. Implicit State Sharing

Notice how Accordion.Header automatically knew how to toggle the panel? The user didn't have to wire up onClick={() => setIndex(1)} manually. It "Just Works".

3. Semantic Structure

It reads like HTML. <List><Item /><Item /></List>. It’s declarative and beautiful.

πŸ•³οΈ The Pitfalls (The "Gotchas")

1. The "Single Child" Restriction

If you try to wrap a child in a <div> that blocks the Context (rare with Context API, but common with the older React.Children.map approach), things break.

Fix: Stick to the Context API pattern shown above. It penetrates through divs like X-rays.

2. Over-Engineering

Don't use this for a simple Button. If a component doesn't have internal state that needs to be shared among multiple distinct children, you probably just need regular props.

3. Name Pollution

If you export Accordion, AccordionItem, AccordionHeader, AccordionPanel, your imports get messy.

Fix: Attach them to the main component: Accordion.Item. It keeps the namespace clean and makes you look like a pro.

🏁 Conclusion
The Compound Component Pattern is the difference between giving someone a fish (a rigid component) and teaching them to fish (a flexible set of tools).

It requires a bit more setup code for you (the library author), but it creates a delightful experience for the developer using your component. And isn't that what we all want? To be loved by other developers? (Please validate me). πŸ₯ΊπŸ€

Top comments (0)