Learn advanced component composition patterns to build flexible, reusable, and maintainable React applications.
Component composition is one of React's most powerful features. Instead of using class inheritance, React uses composition to build complex UIs from simple, reusable components. This lesson covers advanced composition patterns that will make your components more flexible and maintainable.
React favors composition over inheritance. Instead of creating complex class hierarchies, you build functionality by combining simple components.
// Traditional OOP approach (not recommended in React)
class BaseButton extends Component {
render() {
return <button className="btn">{this.props.children}</button>;
}
}
class PrimaryButton extends BaseButton {
render() {
return <button className="btn btn-primary">{this.props.children}</button>;
}
}
class DangerButton extends BaseButton {
render() {
return <button className="btn btn-danger">{this.props.children}</button>;
}
}
// React composition approach (recommended)
function Button({ variant = 'default', children, ...props }) {
return (
<button
className={`btn btn-\${variant}`}
{...props}
>
{children}
</button>
);
}
// Usage - much more flexible
<Button variant="primary">Primary Action</Button>
<Button variant="danger">Delete Item</Button>
<Button variant="success" onClick={handleSave}>Save Changes</Button>
The children prop is the foundation of composition in React. It allows components to be generic containers.
function Card({ title, children }) {
return (
<div className="card">
{title && (
<div className="card-header">
<h3>{title}</h3>
</div>
)}
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage - Card can contain any content
<Card title="User Profile">
<img src="avatar.jpg" alt="User" />
<h4>John Doe</h4>
<p>Software Engineer</p>
<button>Edit Profile</button>
</Card>
<Card title="Statistics">
<div className="stats-grid">
<div className="stat">
<span className="number">1,234</span>
<span className="label">Users</span>
</div>
<div className="stat">
<span className="number">5,678</span>
<span className="label">Posts</span>
</div>
</div>
</Card>
function Container({ size = 'md', children }) {
const sizes = {
sm: 'max-w-4xl',
md: 'max-w-6xl',
lg: 'max-w-7xl'
};
return (
<div className={`container mx-auto px-4 \${sizes[size]}`}>
{children}
</div>
);
}
function Flex({ direction = 'row', gap = '4', children, ...props }) {
return (
<div
className={`flex flex-\${direction} gap-\${gap}`}
{...props}
>
{children}
</div>
);
}
function Grid({ cols = '1', gap = '4', children }) {
return (
<div className={`grid grid-cols-\${cols} gap-\${gap}`}>
{children}
</div>
);
}
// Building layouts with composition
function HomePage() {
return (
<Container size="lg">
<Flex direction="col" gap="8">
<header>
<h1>Welcome to Our App</h1>
</header>
<Flex gap="6">
<main className="flex-1">
<Grid cols="2" gap="6">
<Card title="Recent Posts">
<PostList />
</Card>
<Card title="Popular Topics">
<TopicList />
</Card>
</Grid>
</main>
<aside className="w-80">
<Card title="User Stats">
<UserStats />
</Card>
</aside>
</Flex>
</Flex>
</Container>
);
}
function Dialog({ children, isOpen, onClose }) {
if (!isOpen) return null;
return (
<div className="dialog-overlay" onClick={onClose}>
<div
className="dialog-content"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
}
function DialogHeader({ children }) {
return (
<div className="dialog-header">
{children}
</div>
);
}
function DialogBody({ children }) {
return (
<div className="dialog-body">
{children}
</div>
);
}
function DialogFooter({ children }) {
return (
<div className="dialog-footer">
{children}
</div>
);
}
// Usage - flexible dialog composition
<Dialog isOpen={showConfirm} onClose={() => setShowConfirm(false)}>
<DialogHeader>
<h2>Confirm Delete</h2>
</DialogHeader>
<DialogBody>
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
</DialogBody>
<DialogFooter>
<button onClick={() => setShowConfirm(false)}>Cancel</button>
<button onClick={handleDelete} className="btn-danger">Delete</button>
</DialogFooter>
</Dialog>
Higher-Order Components are functions that take a component and return a new component with additional functionality.
// HOC that adds loading state
function withLoading(WrappedComponent) {
return function WithLoadingComponent(props) {
if (props.isLoading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}
return <WrappedComponent {...props} />;
};
}
// Original components
function UserList({ users }) {
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
// Enhanced components with loading
const UserListWithLoading = withLoading(UserList);
const ProductListWithLoading = withLoading(ProductList);
// Usage
<UserListWithLoading users={users} isLoading={loadingUsers} />
<ProductListWithLoading products={products} isLoading={loadingProducts} />
function withAuth(WrappedComponent) {
return function WithAuthComponent(props) {
const { user, isAuthenticated } = useAuth(); // Custom hook
if (!isAuthenticated) {
return (
<div className="auth-required">
<h2>Authentication Required</h2>
<p>Please log in to access this content.</p>
<LoginButton />
</div>
);
}
return <WrappedComponent {...props} user={user} />;
};
}
// Protect components with authentication
const ProtectedUserProfile = withAuth(UserProfile);
const ProtectedSettings = withAuth(Settings);
const ProtectedDashboard = withAuth(Dashboard);
// Usage
<ProtectedUserProfile userId={123} />
<ProtectedSettings />
<ProtectedDashboard />
function withData(url, propName = 'data') {
return function(WrappedComponent) {
return function WithDataComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error.message);
setLoading(false);
});
}, [url]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
const enhancedProps = {
...props,
[propName]: data,
isLoading: loading,
error: error
};
return <WrappedComponent {...enhancedProps} />;
};
};
}
// Usage
const UserListWithData = withData('/api/users', 'users')(UserList);
const PostListWithData = withData('/api/posts', 'posts')(PostList);
// Components automatically get data
<UserListWithData />
<PostListWithData />
import { compose } from 'redux'; // or create your own compose function
function compose(...fns) {
return (value) => fns.reduceRight((acc, fn) => fn(acc), value);
}
// Combine multiple HOCs
const EnhancedUserList = compose(
withAuth,
withLoading,
withData('/api/users', 'users')
)(UserList);
// Or manually
const EnhancedUserList = withAuth(
withLoading(
withData('/api/users', 'users')(UserList)
)
);
// Usage - component has all enhancements
<EnhancedUserList />
Render props is a pattern where a component takes a function as a prop and calls it to determine what to render.
function DataProvider({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error.message);
setLoading(false);
});
}, [url]);
// Call children as a function with state
return children({ data, loading, error });
}
// Usage
<DataProvider url="/api/users">
{({ data, loading, error }) => {
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return (
<UserList users={data} />
);
}}
</DataProvider>
<DataProvider url="/api/posts">
{({ data, loading, error }) => {
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Failed to load posts</div>;
return (
<div>
<h2>Latest Posts</h2>
<PostGrid posts={data} />
</div>
);
}}
</DataProvider>
function MouseTracker({ children }) {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setMousePosition({
x: event.clientX,
y: event.clientY
});
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return children(mousePosition);
}
// Usage
<MouseTracker>
{({ x, y }) => (
<div>
<h2>Mouse Position</h2>
<p>X: {x}, Y: {y}</p>
<div
style={{
position: 'absolute',
left: x - 10,
top: y - 10,
width: 20,
height: 20,
backgroundColor: 'red',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</div>
)}
</MouseTracker>
function FormProvider({ initialValues = {}, onSubmit, children }) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const setValue = (name, value) => {
setValues(prev => ({
...prev,
[name]: value
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
};
const setTouched = (name) => {
setTouched(prev => ({
...prev,
[name]: true
}));
};
const validate = () => {
const newErrors = {};
// Simple validation rules
Object.keys(values).forEach(key => {
if (!values[key] || values[key].trim() === '') {
newErrors[key] = 'This field is required';
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
onSubmit(values);
}
};
return children({
values,
errors,
touched,
setValue,
setTouched,
handleSubmit,
isValid: Object.keys(errors).length === 0
});
}
// Usage
<FormProvider
initialValues={{ name: '', email: '' }}
onSubmit={(data) => console.log('Form submitted:', data)}
>
{({ values, errors, setValue, setTouched, handleSubmit, isValid }) => (
<form onSubmit={handleSubmit}>
<div className="form-field">
<label>Name</label>
<input
type="text"
value={values.name || ''}
onChange={(e) => setValue('name', e.target.value)}
onBlur={() => setTouched('name')}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div className="form-field">
<label>Email</label>
<input
type="email"
value={values.email || ''}
onChange={(e) => setValue('email', e.target.value)}
onBlur={() => setTouched('email')}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<button type="submit" disabled={!isValid}>
Submit
</button>
</form>
)}
</FormProvider>
Compound components work together to form a complete UI pattern. They share state implicitly and provide a clean API.
import { createContext, useContext, useState } from 'react';
// Create context for tab state
const TabContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">
{children}
</div>
</TabContext.Provider>
);
}
function TabList({ children }) {
return (
<div className="tab-list" role="tablist">
{children}
</div>
);
}
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabContext);
const isActive = activeTab === value;
return (
<button
className={`tab \${isActive ? 'tab-active' : ''}`}
onClick={() => setActiveTab(value)}
role="tab"
aria-selected={isActive}
>
{children}
</button>
);
}
function TabPanels({ children }) {
return (
<div className="tab-panels">
{children}
</div>
);
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabContext);
if (value !== activeTab) return null;
return (
<div className="tab-panel" role="tabpanel">
{children}
</div>
);
}
// Usage - clean, declarative API
<Tabs defaultTab="profile">
<TabList>
<Tab value="profile">Profile</Tab>
<Tab value="settings">Settings</Tab>
<Tab value="billing">Billing</Tab>
</TabList>
<TabPanels>
<TabPanel value="profile">
<UserProfile />
</TabPanel>
<TabPanel value="settings">
<UserSettings />
</TabPanel>
<TabPanel value="billing">
<BillingInfo />
</TabPanel>
</TabPanels>
</Tabs>
const AccordionContext = createContext();
function Accordion({ children, allowMultiple = false }) {
const [openItems, setOpenItems] = useState(new Set());
const toggleItem = (value) => {
setOpenItems(prev => {
const newSet = new Set(prev);
if (newSet.has(value)) {
newSet.delete(value);
} else {
if (!allowMultiple) {
newSet.clear();
}
newSet.add(value);
}
return newSet;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggleItem }}>
<div className="accordion">
{children}
</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ value, children }) {
return (
<div className="accordion-item">
{children}
</div>
);
}
function AccordionTrigger({ value, children }) {
const { openItems, toggleItem } = useContext(AccordionContext);
const isOpen = openItems.has(value);
return (
<button
className={`accordion-trigger \${isOpen ? 'accordion-trigger-open' : ''}`}
onClick={() => toggleItem(value)}
aria-expanded={isOpen}
>
{children}
<span className="accordion-icon">
{isOpen ? '−' : '+'}
</span>
</button>
);
}
function AccordionContent({ value, children }) {
const { openItems } = useContext(AccordionContext);
const isOpen = openItems.has(value);
return (
<div className={`accordion-content \${isOpen ? 'accordion-content-open' : ''}`}>
{children}
</div>
);
}
// Usage
<Accordion allowMultiple={true}>
<AccordionItem value="faq1">
<AccordionTrigger value="faq1">
What is React?
</AccordionTrigger>
<AccordionContent value="faq1">
React is a JavaScript library for building user interfaces.
</AccordionContent>
</AccordionItem>
<AccordionItem value="faq2">
<AccordionTrigger value="faq2">
How do I get started?
</AccordionTrigger>
<AccordionContent value="faq2">
You can start by creating a new React app with Create React App.
</AccordionContent>
</AccordionItem>
</Accordion>
const DropdownContext = createContext();
function Dropdown({ children }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<DropdownContext.Provider value={{ isOpen, setIsOpen }}>
<div className="dropdown" ref={dropdownRef}>
{children}
</div>
</DropdownContext.Provider>
);
}
function DropdownTrigger({ children }) {
const { isOpen, setIsOpen } = useContext(DropdownContext);
return (
<button
className="dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
>
{children}
</button>
);
}
function DropdownMenu({ children }) {
const { isOpen } = useContext(DropdownContext);
if (!isOpen) return null;
return (
<div className="dropdown-menu">
{children}
</div>
);
}
function DropdownItem({ children, onClick }) {
const { setIsOpen } = useContext(DropdownContext);
return (
<button
className="dropdown-item"
onClick={() => {
onClick?.();
setIsOpen(false);
}}
>
{children}
</button>
);
}
// Usage
<Dropdown>
<DropdownTrigger>
User Menu <span>▼</span>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem onClick={() => navigate('/profile')}>
View Profile
</DropdownItem>
<DropdownItem onClick={() => navigate('/settings')}>
Settings
</DropdownItem>
<DropdownItem onClick={handleLogout}>
Sign Out
</DropdownItem>
</DropdownMenu>
</Dropdown>
// Layout components with composition
function Dashboard({ children }) {
return (
<div className="dashboard">
{children}
</div>
);
}
function DashboardHeader({ children }) {
return (
<header className="dashboard-header">
{children}
</header>
);
}
function DashboardSidebar({ children }) {
return (
<aside className="dashboard-sidebar">
{children}
</aside>
);
}
function DashboardMain({ children }) {
return (
<main className="dashboard-main">
{children}
</main>
);
}
function DashboardFooter({ children }) {
return (
<footer className="dashboard-footer">
{children}
</footer>
);
}
// Widget components
function Widget({ title, actions, children }) {
return (
<div className="widget">
<div className="widget-header">
<h3 className="widget-title">{title}</h3>
{actions && (
<div className="widget-actions">
{actions}
</div>
)}
</div>
<div className="widget-content">
{children}
</div>
</div>
);
}
// Usage - flexible dashboard composition
function App() {
return (
<Dashboard>
<DashboardHeader>
<h1>My Dashboard</h1>
<UserMenu />
</DashboardHeader>
<DashboardSidebar>
<Navigation />
</DashboardSidebar>
<DashboardMain>
<Grid cols="2" gap="6">
<Widget
title="Sales Overview"
actions={<RefreshButton />}
>
<SalesChart />
</Widget>
<Widget title="Recent Orders">
<OrdersList />
</Widget>
<Widget
title="User Activity"
actions={
<Dropdown>
<DropdownTrigger>Options</DropdownTrigger>
<DropdownMenu>
<DropdownItem>Export Data</DropdownItem>
<DropdownItem>View Details</DropdownItem>
</DropdownMenu>
</Dropdown>
}
>
<ActivityFeed />
</Widget>
<Widget title="Performance">
<PerformanceMetrics />
</Widget>
</Grid>
</DashboardMain>
<DashboardFooter>
<p>© 2024 My Company</p>
</DashboardFooter>
</Dashboard>
);
}
❌ Bad - Complex prop interface:
function Modal({
isOpen,
title,
content,
showHeader,
showFooter,
primaryAction,
secondaryAction,
headerContent,
footerContent
}) {
return (
<div className="modal">
{showHeader && (
<div className="modal-header">
{headerContent || <h2>{title}</h2>}
</div>
)}
<div className="modal-body">
{content}
</div>
{showFooter && (
<div className="modal-footer">
{footerContent || (
<>
{secondaryAction}
{primaryAction}
</>
)}
</div>
)}
</div>
);
}
✅ Good - Composition approach:
function Modal({ isOpen, children, onClose }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
// Usage - much more flexible
<Modal isOpen={showModal} onClose={closeModal}>
<ModalHeader>
<h2>Custom Title</h2>
<CloseButton onClick={closeModal} />
</ModalHeader>
<ModalBody>
<p>Any content can go here</p>
<CustomForm />
</ModalBody>
<ModalFooter>
<Button variant="secondary" onClick={closeModal}>Cancel</Button>
<Button variant="primary" onClick={handleSave}>Save</Button>
</ModalFooter>
</Modal>
✅ Good - Context provides clean API:
// Components know about each other through context
const FormContext = createContext();
function Form({ children, onSubmit }) {
const [values, setValues] = useState({});
return (
<FormContext.Provider value={{ values, setValues }}>
<form onSubmit={onSubmit}>
{children}
</form>
</FormContext.Provider>
);
}
function Field({ name, children }) {
const { values, setValues } = useContext(FormContext);
return (
<div className="field">
{React.cloneElement(children, {
value: values[name] || '',
onChange: (e) => setValues(prev => ({
...prev,
[name]: e.target.value
}))
})}
</div>
);
}
// Usage
<Form onSubmit={handleSubmit}>
<Field name="username">
<input type="text" placeholder="Username" />
</Field>
<Field name="password">
<input type="password" placeholder="Password" />
</Field>
</Form>
✅ Good - Single responsibility:
// Each HOC has one job
const withAuth = (Component) => (props) => {
const { isAuthenticated } = useAuth();
return isAuthenticated ? <Component {...props} /> : <LoginPrompt />;
};
const withLoading = (Component) => (props) => {
return props.loading ? <LoadingSpinner /> : <Component {...props} />;
};
const withErrorHandling = (Component) => (props) => {
return props.error ? <ErrorMessage error={props.error} /> : <Component {...props} />;
};
// Compose them together
const EnhancedComponent = withAuth(
withLoading(
withErrorHandling(MyComponent)
)
);
Component composition is a powerful pattern that makes React applications more maintainable and flexible:
✅ Use children prop for containment and layout
✅ Apply HOCs for cross-cutting concerns
✅ Leverage render props for flexible data sharing
✅ Build compound components for complex UI patterns
✅ Prefer composition over inheritance
✅ Keep each pattern focused and simple
In the next lesson, we'll dive into React State - how to add interactivity to your components with the useState hook. You'll learn:
State is what makes your components come alive with interactivity!