Advanced Compound Components
Master compound components pattern to create flexible and composable component APIs that work together seamlessly. This is a foundational concept in component-based UI development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world React experience. Take your time with each section and practice the examples
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.. This is an essential concept that every React developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Basic Compound Components
// 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
// 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 — a critical concept in component-based UI development that you will use frequently in real projects
- Provide sensible defaults for all components — a critical concept in component-based UI development that you will use frequently in real projects
- Allow flexible composition order — a critical concept in component-based UI development that you will use frequently in real projects
- Use displayName for better debugging — a critical concept in component-based UI development that you will use frequently in real projects
- Handle edge cases gracefully — a critical concept in component-based UI development that you will use frequently in real projects
- Document the component API clearly — a critical concept in component-based UI development that you will use frequently in real projects
Common Compound Patterns
// 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
// 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>