Compound Components in React JS
Compound Components is an advanced React design pattern that allows multiple related components to work together as a single, flexible UI component. These components share state and behavior while allowing developers to control their structure and layout.
For example, a Tabs component may contain Tabs.List, Tabs.Tab, and Tabs.Panel. Together, these components create a complete tab interface.
Course Resources: JustAcademy React JS Training Course | Register for React JS Course Demo
1. What Are Compound Components?
Compound components are a collection of related components designed to work together. Instead of placing every feature and option inside one large component, developers divide the interface into smaller components with clearly defined responsibilities.
These components often share information through React Context, allowing child components to access common state without passing props through every intermediate component.
Example
<Tabs>
<Tabs.List>
<Tabs.Tab id="profile">Profile</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="profile">Profile content</Tabs.Panel>
<Tabs.Panel id="settings">Settings content</Tabs.Panel>
</Tabs>
Here, Tabs manages the shared state, while its child components display the tab buttons and corresponding content.
2. Why Do We Use Compound Components?
- Flexible layouts: Developers decide where each child component appears.
- Reusable UI: The same component group can be used across different pages.
- Shared state: Related components can communicate through a common parent or Context.
- Cleaner code: Large components can be divided into smaller, manageable components.
- Better maintainability: Each component has a clear responsibility.
- Consistent behavior: Related components follow the same state and interaction rules.
3. How Compound Components Work
- Create a parent component: The parent manages shared state and behavior.
- Create child components: Each child handles a specific part of the interface.
- Create a shared context: Use
createContext() when child components need access to common state.
- Provide context: Wrap the child components in the context provider.
- Consume context: Child components use
useContext() to read shared data and functions.
- Compose the UI: Arrange the child components in the required order.
- Update the interface: When shared state changes, React updates the relevant UI.
Basic Structure
Parent Component
├── Shared State
├── Context Provider
├── Child Component A
├── Child Component B
└── Child Component C
4. Important React Concepts Used
| Concept |
Purpose |
| Components |
Divide the interface into reusable parts. |
| Props |
Pass data and configuration to components. |
children |
Allow nested content to be rendered inside a component. |
useState() |
Store and update local component state. |
createContext() |
Create a context for sharing values between related components. |
useContext() |
Read the nearest matching context value. |
| Composition |
Combine small components to build a complete interface. |
React Context is especially useful when related components need shared information at different nesting levels. However, not every compound component needs Context; simple prop passing or composition may be sufficient for smaller components.
5. Building a Compound Component Using React Context
In this example, we will create a reusable Accordion component. It will contain a parent component, an item component, a title button, and a content panel.
Step 1: Create Accordion.jsx
import { createContext, useContext, useState } from "react";
const AccordionContext = createContext(null);
function Accordion({ children, defaultOpen = null }) {
const [openItem, setOpenItem] = useState(defaultOpen);
const toggleItem = (id) => {
setOpenItem((current) => current === id ? null : id);
};
return (
<AccordionContext value={{ openItem, toggleItem }}>
<div className="accordion">{children}</div>
</AccordionContext>
);
}
function Item({ id, children }) {
return <section className="accordion-item">{children}</section>;
}
function Trigger({ id, children }) {
const context = useContext(AccordionContext);
if (!context) {
throw new Error("Accordion.Trigger must be used inside Accordion");
}
const { openItem, toggleItem } = context;
const isOpen = openItem === id;
return (
<button
type="button"
aria-expanded={isOpen}
aria-controls={`panel-${id}`}
onClick={() => toggleItem(id)}
>
{children} {isOpen ? "−" : "+"}
</button>
);
}
function Panel({ id, children }) {
const context = useContext(AccordionContext);
if (!context) {
throw new Error("Accordion.Panel must be used inside Accordion");
}
const isOpen = context.openItem === id;
return (
<div id={`panel-${id}`} hidden={!isOpen}>
{children}
</div>
);
}
Accordion.Item = Item;
Accordion.Trigger = Trigger;
Accordion.Panel = Panel;
export default Accordion;
Note: This example uses the React 19 context-provider syntax, . In React 18 and earlier, use instead.
Step 2: Use the Accordion Component
import Accordion from "./Accordion";
function App() {
return (
<div>
<h1>Frequently Asked Questions</h1>
<Accordion defaultOpen="question1">
<Accordion.Item id="question1">
<Accordion.Trigger id="question1">
What is React?
</Accordion.Trigger>
<Accordion.Panel id="question1">
React is a JavaScript library for building user interfaces.
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item id="question2">
<Accordion.Trigger id="question2">
What are compound components?
</Accordion.Trigger>
<Accordion.Panel id="question2">
They are related components that work together as one UI pattern.
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</div>
);
}
export default App;
Step 3: Add CSS
.accordion {
max-width: 600px;
margin: 20px auto;
}
.accordion-item {
border: 1px solid #ccc;
margin-bottom: 10px;
border-radius: 6px;
}
.accordion-item button {
width: 100%;
padding: 14px;
text-align: left;
cursor: pointer;
border: none;
background: #f4f4f4;
font-size: 16px;
}
.accordion-item [hidden] {
display: none;
}
.accordion-item [id^="panel-"] {
padding: 14px;
}
How the Example Works
Accordion stores the ID of the currently open item.
Accordion.Item groups the trigger and panel.
Accordion.Trigger changes the open item when clicked.
Accordion.Panel displays content only when its ID matches the open item.
- The context allows the trigger and panel to access shared state without receiving it through every parent component.
This implementation supports one open item at a time. Clicking an already-open trigger closes it. The trigger and panel IDs should be unique within the accordion.
6. Understanding the Compound Component API
A compound component API commonly uses a parent component with named child components attached as properties.
Accordion.Item
Accordion.Trigger
Accordion.Panel
For example, assigning Accordion.Trigger = Trigger allows developers to write instead of importing the trigger component separately. This naming style groups related components under one API.
These properties are a JavaScript organization pattern, not a special React feature. The child components still need the correct context provider when they consume shared state.
7. Compound Components vs. Regular Components
| Regular Components |
Compound Components |
| Often receive their data through individual props. |
Related children can share state through Context. |
| May expose a fixed set of layout options. |
Often allow flexible child composition. |
| Can work independently. |
Usually work together to provide a complete feature. |
| Suitable for simple UI elements. |
Useful for complex, coordinated UI patterns. |
| Example: a standalone Button. |
Example: Tabs, Accordion, Menu, or Select. |
8. Compound Components vs. Prop Drilling
Prop drilling occurs when data is passed through intermediate components that do not directly use it. Compound components can use Context to avoid this when multiple related children need access to the same state.
// Prop drilling
<Parent openItem={openItem} toggleItem={toggleItem}>
<Middle openItem={openItem} toggleItem={toggleItem}>
<Child openItem={openItem} toggleItem={toggleItem} />
</Middle>
</Parent>
// Shared context
<AccordionContext value={{ openItem, toggleItem }}>
<Accordion.Trigger id="question1">Question</Accordion.Trigger>
</AccordionContext>
Use Context when it makes the API simpler. For a small component tree, passing explicit props may be easier to understand and maintain.
9. Common Use Cases
- Tabs: Coordinate tab buttons and their content panels.
- Accordion: Coordinate expandable headings and panels.
- Dropdown Menu: Coordinate the trigger, menu, and menu items.
- Modal: Coordinate the dialog, header, body, and footer.
- Radio Group: Share the selected value across radio options.
- Form Controls: Coordinate labels, inputs, validation messages, and help text.
- Navigation: Coordinate navigation items and active-state information.
- Data Tables: Group headers, rows, cells, and sorting controls under a common API.
10. Advantages of Compound Components
- Reusability: Build a UI pattern once and use it in multiple places.
- Flexible composition: Arrange child components to suit the page design.
- Shared behavior: Keep related state and interaction logic in one place.
- Improved readability: Named child components make the interface structure easier to understand.
- Reduced prop drilling: Context can make shared values accessible to deeply nested children.
- Encapsulation: The parent can hide implementation details behind a clear component API.
11. Disadvantages and Limitations
- Initial implementation can be more complex than a simple component.
- Child components that consume Context must be rendered under the correct provider.
- Shared Context updates may re-render consuming components when its value changes.
- Incorrect or duplicate IDs can cause components to control the wrong panel.
- Excessive use of Context can make data flow harder to trace.
- Compound components may be unnecessary for very small or independent UI elements.
- Accessibility features such as keyboard interaction, focus management, and ARIA attributes must be implemented according to the component's purpose.
12. Best Practices
- Keep the parent component responsible for shared state and behavior.
- Give each child component a clear purpose.
- Use descriptive names such as
Tabs.List, Tabs.Tab, and Tabs.Panel.
- Use Context only when it simplifies communication between related components.
- Provide a meaningful error when a context-consuming child is used outside its parent.
- Use unique and stable identifiers for items and panels.
- Follow accessibility guidelines for buttons, menus, tabs, and other interactive elements.
- Test interactions, keyboard behavior, and invalid usage.
- Document the expected child components and supported props.
- Keep the API flexible without introducing unnecessary abstractions.
13. Common Mistakes
| Mistake |
Solution |
| Using a child outside its required parent. |
Wrap the child in the correct compound component or provider. |
| Forgetting to provide Context. |
Ensure the context provider wraps all consumers. |
| Using duplicate item IDs. |
Assign a unique ID to each item. |
| Putting all logic in one large component. |
Separate the parent, trigger, panel, and other responsibilities. |
| Using Context for every prop. |
Prefer direct props when the data flow is simple. |
| Ignoring accessibility. |
Use semantic HTML, appropriate ARIA attributes, and keyboard support. |
| Creating unstable Context values unnecessarily. |
Keep provider values intentional and optimize only when profiling shows a need. |
14. Testing Compound Components
Testing should focus on what users can see and do, rather than on internal implementation details.
Example with React Testing Library
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import Accordion from "./Accordion";
describe("Accordion", () => {
it("opens and closes a panel", async () => {
const user = userEvent.setup();
render(
<Accordion>
<Accordion.Item id="faq1">
<Accordion.Trigger id="faq1">
What is React?
</Accordion.Trigger>
<Accordion.Panel id="faq1">
React is a UI library.
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
const trigger = screen.getByRole("button", {
name: /what is react/i
});
expect(trigger).toHaveAttribute("aria-expanded", "false");
await user.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("React is a UI library.")).toBeVisible();
await user.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "false");
expect(screen.getByText("React is a UI library.")).not.toBeVisible();
});
});
This test verifies the user-visible behavior: the panel opens when its trigger is clicked and closes when clicked again. The example assumes Vitest, React Testing Library, user-event, and the jest-dom matchers are configured in the project.
15. Practical Project: Build a Tabs Component
Create a reusable Tabs component using the compound component pattern.
Requirements
- Create a parent
Tabs component.
- Create
Tabs.List to group the tab buttons.
- Create
Tabs.Tab to select a tab.
- Create
Tabs.Panel to show matching content.
- Use Context to share the active tab ID and selection function.
- Support a default selected tab.
- Use appropriate tab roles and ARIA attributes.
- Test selecting a tab and displaying its corresponding panel.
Suggested Component Structure
Tabs/
├── Tabs.jsx
├── TabsList.jsx
├── TabsTab.jsx
├── TabsPanel.jsx
├── Tabs.css
└── Tabs.test.jsx
Expected Result
When a user selects the Profile tab, the Profile panel should become active. Selecting Settings should display the Settings panel and update the selected tab state. For a production-quality tabs component, also implement the expected keyboard navigation and focus behavior.
16. Interview Questions and Answers
Q1. What are compound components in React?
Compound components are related components that work together to create a complete UI pattern and may share state or behavior through a parent component or Context.
Q2. Why do we use compound components?
They improve reusability, flexibility, code organization, and coordination between related UI elements.
Q3. What is the role of React Context?
Context makes shared information available to descendant components without passing it explicitly through every intermediate component.
Q4. Are compound components a built-in React feature?
No. They are a design pattern built using standard React features such as components, props, composition, state, and Context.
Q5. Can compound components work without Context?
Yes. They can use props, callbacks, or other state-sharing approaches. Context is useful when related children need shared data across different nesting levels.
Q6. What is prop drilling?
Prop drilling is the process of passing props through intermediate components that do not directly use those values.
Q7. Name some examples of compound components.
Tabs, Accordions, Dropdown Menus, Modals, Radio Groups, and Select components are common examples.
Q8. What does children mean in React?
children is a prop that represents the content nested between a component's opening and closing tags.
Q9. What happens when Context changes?
Components consuming that Context receive the updated value and React renders the relevant updates.
Q10. When should compound components be avoided?
Avoid them when a component is simple, independent, or does not benefit from coordinated state and flexible composition.
Q11. How can you prevent a compound child from being used incorrectly?
Check whether the required Context exists and throw a clear error if the component is rendered outside its expected parent.
Q12. What is the difference between composition and inheritance in React?
Composition combines components through nesting and props. React generally favors composition to reuse UI behavior instead of relying on class inheritance.
17. Practice Exercises
- Create an Accordion with three questions and answers.
- Build a Tabs component with Profile, Settings, and Notifications tabs.
- Create a Dropdown Menu with a trigger and menu items.
- Build a Modal with Header, Body, and Footer subcomponents.
- Add tests for opening, closing, and selecting items.
- Improve keyboard navigation and accessibility.
18. Quick Revision
- Compound components are a reusable React design pattern.
- A parent component commonly manages shared state.
- Child components handle separate parts of the UI.
- Context can share state and functions with nested children.
- Named child components create a clear and flexible API.
- Common examples include Tabs, Accordions, and Dropdown Menus.
- Use Context thoughtfully and preserve accessibility.
- Test the behavior from the user's perspective.
19. Official Learning Resources
20. Conclusion
Compound Components help developers create flexible, reusable, and maintainable React interfaces by combining related components under one shared design. With component composition, state management, and React Context, developers can build patterns such as Tabs, Accordions, Modals, and Dropdown Menus with a clean API and coordinated behavior.
To continue learning, visit JustAcademy React JS Training Course or Register for React JS Course Demo.