Hey there, Fellow Dev! 👋
Have you ever tried to build a reusable component, like a generic Tabs or Accordion, and found yourself passing a million props just to configure it?
You end up with something scary like this:
// The "God Component" approach
<Tabs
data={[
{ label: "Home", content: "Welcome home!" },
{ label: "Profile", content: "User profile here." }
]}
theme="dark"
activeTabIndex={0}
onTabChange={handleTabChange}
tabClassName="text-bold"
panelClassName="p-4"
/>
It works, but it’s rigid. What if you want to add an icon to just one tab? Or disable the second tab? You’d have to add more properties to that data array. It gets messy fast. 🤯
Enter the Compound Component Pattern.
What is it?
Think about standard HTML tags like <select> and <option>. They don’t work alone; they work together.
<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
The <select> manages the state, and the <option>s render the choices. They share state implicitly. We can do the same in React!
Step 1: Create the Context
First, we need a way for the parent (Tabs) to talk to its children (Tab and TabPanel) without passing props manually. We use React Context for this.
import React, { createContext, useContext, useState } from 'react';
// 1. Create the Context
const TabsContext = createContext();
// 2. Create the Parent Component
const Tabs = ({ children, defaultIndex = 0 }) => {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs-container">
{children}
</div>
</TabsContext.Provider>
);
};
Here, Tabs holds the state (activeIndex) and provides it to anyone inside it.
Step 2: Create the Sub-Components
Now, let’s create the components that will consume this context.
// 3. Create the Tab List (optional wrapper for styling)
const TabList = ({ children }) => {
return <div className="flex border-b">{children}</div>;
};
// 4. Create the individual Tab
const Tab = ({ index, children }) => {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isActive = activeIndex === index;
return (
<button
onClick={() => setActiveIndex(index)}
className={`p-2 ${isActive ? 'border-b-2 border-blue-500 font-bold' : 'text-gray-500'}`}
>
{children}
</button>
);
};
// 5. Create the Panel
const TabPanel = ({ index, children }) => {
const { activeIndex } = useContext(TabsContext);
if (activeIndex !== index) return null;
return <div className="p-4">{children}</div>;
};
Step 3: Use It!
Now look how clean and flexible our API is:
const App = () => {
return (
<Tabs defaultIndex={0}>
<TabList>
<Tab index={0}>🏠 Home</Tab>
<Tab index={1}>👤 Profile</Tab>
<Tab index={2}>⚙️ Settings</Tab>
</TabList>
<TabPanel index={0}>
<h2>Welcome to the Dashboard</h2>
<p>This uses the Compound Component pattern!</p>
</TabPanel>
<TabPanel index={1}>
<UserProfile />
</TabPanel>
<TabPanel index={2}>
<p>Settings go here...</p>
</TabPanel>
</Tabs>
);
};
Why is this better?
- Flexibility: I can easily swap the order of tabs, wrap them in divs for styling, or add icons without changing the
Tabslogic. - Readability: It looks like HTML. You can see the structure immediately.
- Separation of Concerns:
Tabshandles the logic (state), whileTabandTabPanelhandle the rendering.
When to use it?
Use this pattern when you have a group of components that need to share state and logic implicitly, like:
- Accordions (Item, Header, Content)
- Tabs (List, Tab, Panel)
- Dropdown Menus (Button, Menu, Item)
- Form Groups (Label, Input, ErrorMessage)
Closing
The Compound Component pattern is a powerful tool in your React arsenal. It shifts the complexity from the consumer (you) to the library author (also you, but the future you).
Start using it for your reusable UI components, and your future self will thank you!
Happy Coding! 🚀