Compound Components

0/3 in this phase0/36 across the roadmap

📖 Concept

Compound Components is a pattern where several components work together to maintain a shared implicit state. Think of the HTML <select> and <option> elements. You don't pass an array of options to the select; instead, you nest the options inside.

Benefits:

  • Reduced Prop Drilling: You don't need to pass many props to a single "god" component.
  • Flexibility: The consumer (developer using your component) has control over the order and placement of sub-components.
  • Clean API: The usage looks more like standard HTML.

Implementation: Typically implemented using the Context API to share state between the parent and its children without passing props explicitly.

💻 Code Example

codeTap to expand ⛶
1import React, { useState, useContext, createContext } from 'react';
2
3// 1. Create a Context for the compound component
4const AccordionContext = createContext();
5
6// 2. Parent Component (Provider)
7function Accordion({ children }) {
8 const [openIndex, setOpenIndex] = useState(null);
9
10 const toggle = (index) => {
11 setOpenIndex(openIndex === index ? null : index);
12 };
13
14 return (
15 <AccordionContext.Provider value={{ openIndex, toggle }}>
16 <div className="border rounded-md divide-y overflow-hidden shadow-sm">
17 {children}
18 </div>
19 </AccordionContext.Provider>
20 );
21}
22
23// 3. Child Components (Consumers)
24function AccordionItem({ index, children }) {
25 return (
26 <div className="bg-white">
27 {React.Children.map(children, (child) =>
28 React.cloneElement(child, { index })
29 )}
30 </div>
31 );
32}
33
34function AccordionHeader({ index, children }) {
35 const { openIndex, toggle } = useContext(AccordionContext);
36 const isOpen = openIndex === index;
37
38 return (
39 <button
40 className="w-full px-4 py-3 text-left font-medium flex justify-between items-center hover:bg-gray-50 transition"
41 onClick={() => toggle(index)}
42 >
43 {children}
44 <span>{isOpen ? '−' : '+'}</span>
45 </button>
46 );
47}
48
49function AccordionPanel({ index, children }) {
50 const { openIndex } = useContext(AccordionContext);
51 if (openIndex !== index) return null;
52
53 return (
54 <div className="px-4 py-3 bg-gray-50 text-gray-700 animate-in fade-in duration-200">
55 {children}
56 </div>
57 );
58}
59
60// 4. Attach sub-components to the parent for easier access (optional)
61Accordion.Item = AccordionItem;
62Accordion.Header = AccordionHeader;
63Accordion.Panel = AccordionPanel;
64
65// 5. Usage:
66function App() {
67 return (
68 <Accordion>
69 <Accordion.Item index={0}>
70 <Accordion.Header>What is React?</Accordion.Header>
71 <Accordion.Panel>React is a JavaScript library for building UIs.</Accordion.Panel>
72 </Accordion.Item>
73 <Accordion.Item index={1}>
74 <Accordion.Header>Why use Compound Components?</Accordion.Header>
75 <Accordion.Panel>Because they provide a clean and flexible API!</Accordion.Panel>
76 </Accordion.Item>
77 </Accordion>
78 );
79}
80
81export default App;

🏋️ Practice Exercise

  1. Build a 'Tabs' component using the Compound Components pattern (Tabs, TabList, Tab, TabPanel).
  2. Create a 'Modal' component with 'Modal.Trigger', 'Modal.Window', and 'Modal.Close'.
  3. Implement a 'MultiStepForm' where the state of the current step is shared implicitly.
  4. Try to implement the pattern without using React.Children.map (use Context instead). Discuss the pros and cons.

⚠️ Common Mistakes

  • Not providing a fallback for the Context, leading to errors if children are used outside the parent.

  • Over-using the pattern for simple components that don't need it.

  • Breaking the implicit contract by nesting components too deeply (Context solves this!).

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Compound Components