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

Published: (February 16, 2026 at 06:07 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

🎨 The Graphical Explanation

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.

+-----------------------------+
|       |
+-----------------------------+
| [Tab 1] [Tab 2] [Tab 3]     |
   {/* You put this where YOU want */}
    One
    Two


    Content 1
    Content 2

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

💻 The Code: From “Prop Hell” to “Component Heaven”

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 (
    {/* Provide context to children */}
    {children}
  );
};

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 {children};
};

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

  return (
    <div onClick={() => toggleIndex(index)}>
      {children} {isActive ? '🔽' : '▶️'}
    </div>
  );
};

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

  if (openIndex !== index) return null;

  return <div>{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;

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>
);

🏆 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 a Tab, or move the label below an input. Compound components let them do that without adding a renderLabelBottom prop.

  2. Implicit State Sharing
    Notice how Accordion.Header automatically knows 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: “. It’s declarative and beautiful.

🕳️ The Pitfalls (The “Gotchas”)

  1. The “Single Child” Restriction
    If you wrap a child in a “ that blocks the context (rare with the Context API, but common with older React.Children.map approaches), 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
    Exporting Accordion, AccordionItem, AccordionHeader, AccordionPanel can make imports 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 far more ergonomic and extensible API for your users.

It's 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). 🥺🤝
0 views
Back to Blog

Related posts

Read more »