Advanced Compound Components
Master compound components pattern to create flexible and composable component APIs that work together seamlessly.
90 min•By Priygop Team•Last updated: Feb 2026
Understanding Compound Components
Compound components are a pattern where multiple components work together to form a complete UI. They share implicit state and provide a more flexible API than a single monolithic component.
Basic Compound Components
Example
// Basic compound component example
const Tabs = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<div className="tabs">
{React.Children.map(children, child =>
React.cloneElement(child, { activeTab, setActiveTab })
)}
</div>
);
};
const TabList = ({ children, activeTab, setActiveTab }) => (
<div className="tab-list">
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isActive: activeTab === index,
onClick: () => setActiveTab(index)
})
)}
</div>
);
const Tab = ({ children, isActive, onClick }) => (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={onClick}
>
{children}
</button>
);
const TabPanels = ({ children, activeTab }) => (
<div className="tab-panels">
{children[activeTab]}
</div>
);
const TabPanel = ({ children }) => (
<div className="tab-panel">
{children}
</div>
);
// Usage
<Tabs defaultTab={0}>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>Content 1</TabPanel>
<TabPanel>Content 2</TabPanel>
</TabPanels>
</Tabs>Advanced Compound Patterns
Example
// Using Context for compound components
const SelectContext = createContext();
const Select = ({ children, value, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const contextValue = {
value,
onChange,
isOpen,
setIsOpen
};
return (
<SelectContext.Provider value={contextValue}>
<div className="select">
{children}
</div>
</SelectContext.Provider>
);
};
const SelectTrigger = ({ children }) => {
const { isOpen, setIsOpen } = useContext(SelectContext);
return (
<button
className="select-trigger"
onClick={() => setIsOpen(!isOpen)}
>
{children}
</button>
);
};
const SelectContent = ({ children }) => {
const { isOpen } = useContext(SelectContext);
if (!isOpen) return null;
return (
<div className="select-content">
{children}
</div>
);
};
const SelectItem = ({ value, children }) => {
const { value: selectedValue, onChange, setIsOpen } = useContext(SelectContext);
const handleClick = () => {
onChange(value);
setIsOpen(false);
};
return (
<div
className={`select-item ${selectedValue === value ? 'selected' : ''}`}
onClick={handleClick}
>
{children}
</div>
);
};Compound Component Best Practices
- Use Context for complex state sharing
- Provide sensible defaults for all components
- Allow flexible composition order
- Use displayName for better debugging
- Handle edge cases gracefully
- Document the component API clearly
Common Compound Patterns
Example
// Accordion compound component
const Accordion = ({ children, allowMultiple = false }) => {
const [openItems, setOpenItems] = useState(new Set());
const toggleItem = (itemId) => {
setOpenItems(prev => {
const newSet = new Set(prev);
if (newSet.has(itemId)) {
newSet.delete(itemId);
} else {
if (!allowMultiple) {
newSet.clear();
}
newSet.add(itemId);
}
return newSet;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggleItem }}>
{children}
</AccordionContext.Provider>
);
};
const AccordionItem = ({ children, itemId }) => (
<div className="accordion-item">
{React.Children.map(children, child =>
React.cloneElement(child, { itemId })
)}
</div>
);
const AccordionTrigger = ({ children, itemId }) => {
const { openItems, toggleItem } = useContext(AccordionContext);
const isOpen = openItems.has(itemId);
return (
<button
className={`accordion-trigger ${isOpen ? 'open' : ''}`}
onClick={() => toggleItem(itemId)}
>
{children}
</button>
);
};Mini-Project: Form Compound Component
Example
// Complete form compound component system
import React, { createContext, useContext, useState } from 'react';
const FormContext = createContext();
const Form = ({ children, onSubmit, initialValues = {} }) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const setValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const setError = (name, error) => {
setErrors(prev => ({ ...prev, [name]: error }));
};
const setTouchedField = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};
const contextValue = {
values,
errors,
touched,
setValue,
setError,
setTouchedField
};
return (
<FormContext.Provider value={contextValue}>
<form onSubmit={handleSubmit}>
{children}
</form>
</FormContext.Provider>
);
};
const FormField = ({ name, children }) => {
const { values, errors, touched, setValue, setTouchedField } = useContext(FormContext);
const value = values[name] || '';
const error = errors[name];
const isTouched = touched[name];
return (
<div className="form-field">
{React.Children.map(children, child =>
React.cloneElement(child, {
name,
value,
error,
isTouched,
onChange: (e) => setValue(name, e.target.value),
onBlur: () => setTouchedField(name)
})
)}
</div>
);
};
const FormInput = ({ name, value, error, isTouched, onChange, onBlur, ...props }) => (
<div>
<input
{...props}
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
className={`form-input ${error && isTouched ? 'error' : ''}`}
/>
{error && isTouched && (
<span className="form-error">{error}</span>
)}
</div>
);
const FormLabel = ({ children, ...props }) => (
<label className="form-label" {...props}>
{children}
</label>
);
const FormButton = ({ children, type = "submit", ...props }) => (
<button type={type} className="form-button" {...props}>
{children}
</button>
);
// Usage
<Form onSubmit={(values) => console.log(values)}>
<FormField name="email">
<FormLabel>Email</FormLabel>
<FormInput type="email" placeholder="Enter your email" />
</FormField>
<FormField name="password">
<FormLabel>Password</FormLabel>
<FormInput type="password" placeholder="Enter your password" />
</FormField>
<FormButton>Submit</FormButton>
</Form>