Compound Components
Compound components let you express hierarchical UI relationships in JSX while sharing implicit state. Think <Select> with <Select.Option>, or <Tabs> with <Tabs.Tab>. The parent manages state; children communicate through context without explicit props.
Compound Component Pattern
import { createContext, useContext, useState, type ReactNode } from 'react';
// ---- Accordion — compound component ----
interface AccordionContextValue {
openItems: Set<string>;
toggle: (id: string) => void;
allowMultiple: boolean;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordionContext() {
const ctx = useContext(AccordionContext);
if (!ctx) throw new Error('Accordion components must be used within <Accordion>');
return ctx;
}
// Root component — owns state
function Accordion({ children, allowMultiple = false }: { children: ReactNode; allowMultiple?: boolean }) {
const [openItems, setOpenItems] = useState<Set<string>>(new Set());
function toggle(id: string) {
setOpenItems(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!allowMultiple) next.clear(); // close others
next.add(id);
}
return next;
});
}
return (
<AccordionContext.Provider value={{ openItems, toggle, allowMultiple }}>
<div role="region">{children}</div>
</AccordionContext.Provider>
);
}
// Item component — uses parent context
function AccordionItem({ id, children }: { id: string; children: ReactNode }) {
return <div className="accordion-item">{children}</div>;
}
// Trigger component — reads/updates context
function AccordionTrigger({ id, children }: { id: string; children: ReactNode }) {
const { openItems, toggle } = useAccordionContext();
const isOpen = openItems.has(id);
return (
<button
onClick={() => toggle(id)}
aria-expanded={isOpen}
aria-controls={`panel-${id}`}
style={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', background: 'transparent', border: 'none', cursor: 'pointer', textAlign: 'left', fontWeight: 600 }}
>
{children}
<span style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0)', transition: 'transform 0.2s' }}>▼</span>
</button>
);
}
// Panel component — conditionally shown
function AccordionPanel({ id, children }: { id: string; children: ReactNode }) {
const { openItems } = useAccordionContext();
const isOpen = openItems.has(id);
return (
<div
id={`panel-${id}`}
role="region"
hidden={!isOpen}
style={{ padding: isOpen ? '0 20px 16px' : 0, overflow: 'hidden', transition: 'padding 0.2s' }}
>
{children}
</div>
);
}
// Attach sub-components to parent (dot notation API)
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Panel = AccordionPanel;
// ---- Usage — reads like HTML ----
function FAQ() {
return (
<Accordion allowMultiple>
<Accordion.Item id="q1">
<Accordion.Trigger id="q1">What is React?</Accordion.Trigger>
<Accordion.Panel id="q1">React is a JavaScript library for building user interfaces.</Accordion.Panel>
</Accordion.Item>
<Accordion.Item id="q2">
<Accordion.Trigger id="q2">What are hooks?</Accordion.Trigger>
<Accordion.Panel id="q2">Hooks let you use state and lifecycle features in function components.</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}Tip
Tip
Practice Compound Components in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Modern React: Use Hooks instead of class lifecycle methods
Practice Task
Note
Practice Task — (1) Write a working example of Compound Components from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Compound Components is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready react code.