React Compound Components: The 'Build Your Own Adventure' Pattern
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)
-
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 arenderLabelBottomprop. -
Implicit State Sharing
Notice howAccordion.Headerautomatically knows how to toggle the panel? The user didn’t have to wire uponClick={() => setIndex(1)}manually. It just works. -
Semantic Structure
It reads like HTML: “. It’s declarative and beautiful.
🕳️ The Pitfalls (The “Gotchas”)
-
The “Single Child” Restriction
If you wrap a child in a “ that blocks the context (rare with the Context API, but common with olderReact.Children.mapapproaches), things break.
Fix: Stick to the Context API pattern shown above. It penetrates through divs like X‑rays. -
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. -
Name Pollution
ExportingAccordion,AccordionItem,AccordionHeader,AccordionPanelcan 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). 🥺🤝