The Power of Composition
Composition is one of the most powerful concepts in React. It allows you to build complex UIs by combining simpler, reusable components. This approach creates more maintainable, flexible code compared to inheritance-based designs.
Analogy: Building with LEGO vs. Carving a Statue
Think of component composition like building with LEGO blocks, compared to inheritance which is more like carving a statue:
- Composition (LEGO): You build complex structures by connecting simpler, independent pieces. Each piece has a specific purpose, and you can rearrange them in countless ways.
- Inheritance (Statue Carving): You start with a big block and chip away to reveal the desired form. Once carved, it's difficult to repurpose parts or significantly change the structure.
React's philosophy of "composition over inheritance" is like preferring LEGO building over statue carving - it creates more flexible, reusable, and maintainable code.
Containment with the Children Prop
The children prop is a special prop that allows components to pass JSX elements to other components. It's one of the most fundamental composition patterns in React.
Basic Children Usage
function Card({ children, title }) {
return (
<div className="card">
{title && <div className="card-header">
<h2>{title}</h2>
</div>}
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<div className="app">
<Card title="User Profile">
<p>Name: John Doe</p>
<p>Email: john@example.com</p>
<button>Edit Profile</button>
</Card>
<Card title="Recent Activity">
<ul>
<li>Logged in 2 hours ago</li>
<li>Updated profile picture</li>
<li>Posted a new comment</li>
</ul>
</Card>
</div>
);
}
Multiple Named Content Slots
While children provides a single content slot, sometimes you need multiple slots. You can achieve this using additional props:
function PageLayout({ header, sidebar, main, footer }) {
return (
<div className="page-layout">
<header className="header">
{header}
</header>
<div className="content-area">
<aside className="sidebar">
{sidebar}
</aside>
<main className="main-content">
{main}
</main>
</div>
<footer className="footer">
{footer}
</footer>
</div>
);
}
// Usage
function App() {
return (
<PageLayout
header={<h1>My Application</h1>}
sidebar={
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
}
main={
<div>
<h2>Welcome to My App</h2>
<p>This is the main content area.</p>
</div>
}
footer={
<p>© 2025 My Company</p>
}
/>
);
}
Real-World Example: Material-UI's Grid System
Libraries like Material-UI use this pattern extensively. For example, their Grid component:
import { Grid, Paper, Typography } from '@material-ui/core';
function Dashboard() {
return (
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper>
<Typography variant="h4">Dashboard</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper>
<Typography variant="h6">Recent Sales</Typography>
<SalesChart />
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper>
<Typography variant="h6">Customer Activity</Typography>
<ActivityList />
</Paper>
</Grid>
</Grid>
);
}
The Grid component uses children to create complex layouts while handling the responsive behavior internally.
Specialization Pattern
Specialization is when you create a component that's a specific version of another component. This pattern is similar to inheritance but implemented through composition.
Basic Specialization
function Dialog({ title, message, children }) {
return (
<div className="dialog">
<div className="dialog-header">
<h2>{title}</h2>
</div>
<div className="dialog-body">
<p>{message}</p>
{children}
</div>
</div>
);
}
// Specialized version of Dialog
function ConfirmationDialog({ onConfirm, onCancel, ...props }) {
return (
<Dialog {...props}>
<div className="dialog-actions">
<button onClick={onCancel}>Cancel</button>
<button onClick={onConfirm} className="primary">Confirm</button>
</div>
</Dialog>
);
}
// Usage
function App() {
const handleDelete = () => {
console.log('Item deleted');
};
const handleCancel = () => {
console.log('Operation cancelled');
};
return (
<div>
<h1>My App</h1>
<ConfirmationDialog
title="Delete Item"
message="Are you sure you want to delete this item? This action cannot be undone."
onConfirm={handleDelete}
onCancel={handleCancel}
/>
</div>
);
}
Creating a Component Library
The specialization pattern is particularly useful when building a component library with variations:
// Base Button component
function Button({
children,
onClick,
disabled = false,
className = '',
type = 'button',
...rest
}) {
const buttonClass = `button ${className}`.trim();
return (
<button
type={type}
className={buttonClass}
onClick={onClick}
disabled={disabled}
{...rest}
>
{children}
</button>
);
}
// Specialized Button variants
function PrimaryButton(props) {
return <Button className="button-primary" {...props} />;
}
function SecondaryButton(props) {
return <Button className="button-secondary" {...props} />;
}
function DangerButton(props) {
return <Button className="button-danger" {...props} />;
}
function IconButton({ icon, children, ...props }) {
return (
<Button className="button-icon" {...props}>
{icon}
{children && <span className="button-text">{children}</span>}
</Button>
);
}
// Usage
function App() {
return (
<div>
<h1>Button Examples</h1>
<div className="button-container">
<PrimaryButton onClick={() => console.log('Primary clicked')}>
Primary
</PrimaryButton>
<SecondaryButton onClick={() => console.log('Secondary clicked')}>
Secondary
</SecondaryButton>
<DangerButton onClick={() => console.log('Danger clicked')}>
Delete
</DangerButton>
<IconButton
icon={<span className="icon">🔍</span>}
onClick={() => console.log('Search clicked')}
>
Search
</IconButton>
<IconButton
icon={<span className="icon">⚙️</span>}
onClick={() => console.log('Settings clicked')}
/>
</div>
</div>
);
}
Composition vs. Inheritance
React's documentation recommends composition over inheritance. Here's why:
- Flexibility: Composition makes it easier to reuse code between components
- Explicitness: Props and composition make the data flow clear and explicit
- Isolation: Components are more isolated and focused on specific responsibilities
- Testability: Composed components are typically easier to test
When you're tempted to use inheritance, try composition first!
Compound Components Pattern
Compound components are a pattern where multiple components work together to form a cohesive UI. This pattern provides a more declarative and flexible API for complex components.
Basic Compound Components
// Accordion component set
function Accordion({ children }) {
const [activeIndex, setActiveIndex] = useState(null);
// Clone and enhance children with context
const items = React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
isActive: index === activeIndex,
onActivate: () => setActiveIndex(index === activeIndex ? null : index)
});
});
return <div className="accordion">{items}</div>;
}
function AccordionItem({ title, children, isActive, onActivate }) {
return (
<div className="accordion-item">
<div className="accordion-header" onClick={onActivate}>
<h3>{title}</h3>
<span className={`accordion-icon ${isActive ? 'active' : ''}`}>
▼
</span>
</div>
{isActive && (
<div className="accordion-content">
{children}
</div>
)}
</div>
);
}
// Usage
function FAQ() {
return (
<div>
<h2>Frequently Asked Questions</h2>
<Accordion>
<AccordionItem title="What is React?">
<p>React is a JavaScript library for building user interfaces.</p>
</AccordionItem>
<AccordionItem title="How do I install React?">
<p>You can install React using npm: npm install react react-dom</p>
</AccordionItem>
<AccordionItem title="What is JSX?">
<p>JSX is a syntax extension for JavaScript that resembles HTML and makes it easier to write React components.</p>
</AccordionItem>
</Accordion>
</div>
);
}
Using Context for Compound Components
A more advanced implementation uses React Context to share state between components:
import React, { createContext, useContext, useState } from 'react';
// Create context for the Tabs component
const TabsContext = createContext();
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
// Provide context values to children
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs-container">
{children}
</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ children, index }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isActive = activeIndex === index;
return (
<div
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{children}
</div>
);
}
function TabPanels({ children }) {
const { activeIndex } = useContext(TabsContext);
// Only render the active panel
const activePanel = React.Children.toArray(children)[activeIndex];
return <div className="tab-panels">{activePanel}</div>;
}
function TabPanel({ children }) {
return <div className="tab-panel">{children}</div>;
}
// Expose components as properties of the main component
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanels = TabPanels;
Tabs.TabPanel = TabPanel;
// Usage
function ProductInformation() {
return (
<div className="product-info">
<h2>Product XYZ</h2>
<Tabs>
<Tabs.TabList>
<Tabs.Tab index={0}>Description</Tabs.Tab>
<Tabs.Tab index={1}>Specifications</Tabs.Tab>
<Tabs.Tab index={2}>Reviews</Tabs.Tab>
</Tabs.TabList>
<Tabs.TabPanels>
<Tabs.TabPanel>
<p>This is an amazing product that will change your life.</p>
</Tabs.TabPanel>
<Tabs.TabPanel>
<ul>
<li>Dimensions: 10" x 5" x 2"</li>
<li>Weight: 2 lbs</li>
<li>Material: Aluminum</li>
</ul>
</Tabs.TabPanel>
<Tabs.TabPanel>
<p>No reviews yet.</p>
</Tabs.TabPanel>
</Tabs.TabPanels>
</Tabs>
</div>
);
}
Real-World Example: React Bootstrap
Many UI libraries use the compound components pattern. For example, React Bootstrap's Dropdown:
import { Dropdown } from 'react-bootstrap';
function UserMenu() {
return (
<Dropdown>
<Dropdown.Toggle variant="success" id="dropdown-basic">
User Menu
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item href="#/profile">Profile</Dropdown.Item>
<Dropdown.Item href="#/settings">Settings</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item href="#/logout">Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}
This pattern provides a clean, declarative API while maintaining logical grouping of related components.
Render Props Pattern
The render props pattern involves passing a function as a prop that returns a React element. This allows the parent component to control what gets rendered while the child handles the logic.
Basic Render Props
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (e) {
setError(e.message);
setData(null);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return render({ data, loading, error });
}
// Usage
function UserList() {
return (
<div>
<h2>User List</h2>
<DataFetcher
url="https://jsonplaceholder.typicode.com/users"
render={({ data, loading, error }) => {
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No users found</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
);
}}
/>
</div>
);
}
// Another usage with different rendering
function UserCount() {
return (
<div>
<h2>User Statistics</h2>
<DataFetcher
url="https://jsonplaceholder.typicode.com/users"
render={({ data, loading, error }) => {
if (loading) return <div>Loading data...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data found</div>;
return (
<div className="stat-card">
<h3>Total Users</h3>
<div className="stat-value">{data.length}</div>
</div>
);
}}
/>
</div>
);
}
Children as a Function
A common variant of the render props pattern uses the children prop as a function:
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({
x: e.clientX,
y: e.clientY
});
};
return (
<div
className="mouse-tracker"
style={{ height: '300px', border: '1px solid #ccc' }}
onMouseMove={handleMouseMove}
>
{/* Call children as a function with the position data */}
{children(position)}
</div>
);
}
// Usage with children as a function
function App() {
return (
<div>
<h1>Mouse Position Tracker</h1>
<MouseTracker>
{(position) => (
<div>
<p>Current mouse position:</p>
<p>
X: {position.x}, Y: {position.y}
</p>
{/* Render a dot that follows the mouse */}
<div
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: 'red',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none'
}}
/>
</div>
)}
</MouseTracker>
</div>
);
}
Render Props vs. Hooks
With the introduction of React Hooks, many use cases for render props can now be implemented with custom hooks:
// Converting the MouseTracker render prop to a hook
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({
x: e.clientX,
y: e.clientY
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return position;
}
// Usage with hook
function MouseIndicator() {
const position = useMousePosition();
return (
<div>
<p>Current mouse position:</p>
<p>
X: {position.x}, Y: {position.y}
</p>
</div>
);
}
Hooks often provide a cleaner, more reusable solution, but render props are still valuable in certain scenarios, especially for component libraries that need to support a wide range of use cases.
Higher-Order Components (HOCs)
Higher-Order Components are functions that take a component and return a new enhanced component. This pattern was common before hooks but is still useful in certain scenarios.
Basic HOC Pattern
// HOC that adds loading state
function withLoading(WrappedComponent) {
// Return a new component
return function WithLoading({ isLoading, ...props }) {
// Render loading indicator when isLoading is true
if (isLoading) {
return <div className="loading-spinner">Loading...</div>;
}
// Otherwise, render the wrapped component
return <WrappedComponent {...props} />;
};
}
// Basic component
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Enhanced component with loading state
const UserListWithLoading = withLoading(UserList);
// Usage
function App() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return (
<div>
<h1>Users</h1>
<UserListWithLoading isLoading={loading} users={users} />
</div>
);
}
HOC with Additional Props
HOCs can add extra props or behavior to components:
// HOC that adds authentication checking
function withAuth(WrappedComponent) {
return function WithAuth(props) {
const { isAuthenticated, user } = useAuth(); // Custom hook for auth state
if (!isAuthenticated) {
return <Navigate to="/login" />; // Redirect to login
}
// Pass the user prop along with original props
return <WrappedComponent {...props} user={user} />;
};
}
// Dashboard that requires authentication
function Dashboard({ user, ...props }) {
return (
<div className="dashboard">
<h1>Welcome, {user.name}</h1>
<p>Your role: {user.role}</p>
{/* Dashboard content */}
</div>
);
}
// Create authenticated version of Dashboard
const AuthenticatedDashboard = withAuth(Dashboard);
// Usage in routes
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<AuthenticatedDashboard />} />
</Routes>
);
}
Composing Multiple HOCs
HOCs can be composed together to add multiple enhancements:
// HOC for logging props
function withLogger(WrappedComponent) {
return function WithLogger(props) {
console.log(`Component ${WrappedComponent.name} rendered with props:`, props);
return <WrappedComponent {...props} />;
};
}
// HOC for error boundaries
function withErrorBoundary(WrappedComponent) {
return class WithErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
console.error('Error caught by boundary:', error, info);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong!</h2>
<p>{this.state.error.toString()}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return <WrappedComponent {...this.props} />;
}
};
}
// Apply multiple HOCs (composition)
const EnhancedComponent = withErrorBoundary(withLogger(withAuth(Dashboard)));
// Alternative using compose function (from a library like Recompose or lodash/fp)
// const EnhancedComponent = compose(
// withErrorBoundary,
// withLogger,
// withAuth
// )(Dashboard);
HOCs vs. Hooks
While custom hooks have largely replaced HOCs in modern React development, HOCs still have their place:
| HOCs Advantages | Custom Hooks Advantages |
|---|---|
| Can wrap entire components, including render logic | More composable and flexible |
| Can be applied to class components | No component nesting or "wrapper hell" |
| Can modify props and the component tree | Better TypeScript support |
| Work well with the react-redux connect pattern | More straightforward testing |
In modern React development, hooks are generally preferred for sharing behavior, but HOCs are still valuable for cross-cutting concerns like error boundaries or authentication.
Component Composition Best Practices
Keep Components Focused
- Single Responsibility Principle: Each component should do one thing well
- Small Components: Prefer smaller, focused components over large, complex ones
- Separation of Concerns: Separate UI, logic, and state management
Design for Composition
- Accept and Use Children: Embrace the children prop for flexibility
- Prefer Composition Over Configuration: Use composition to handle variations instead of complex props
- Logical Grouping: Group related components (e.g., Form and Form.Input)
Example: Configurable Card Component
// Less flexible, prop-heavy approach
function Card({
title,
subtitle,
content,
image,
actions,
footerText,
variant,
size,
// many more configuration props...
}) {
// Complex conditional rendering based on numerous props
}
// More flexible, composition-based approach
function Card({ children, variant = 'default', size = 'medium' }) {
return (
<div className={`card card-${variant} card-${size}`}>
{children}
</div>
);
}
Card.Header = function CardHeader({ children }) {
return <div className="card-header">{children}</div>;
};
Card.Title = function CardTitle({ children }) {
return <h3 className="card-title">{children}</h3>;
};
Card.Subtitle = function CardSubtitle({ children }) {
return <div className="card-subtitle">{children}</div>;
};
Card.Image = function CardImage({ src, alt }) {
return <img className="card-image" src={src} alt={alt} />;
};
Card.Content = function CardContent({ children }) {
return <div className="card-content">{children}</div>;
};
Card.Actions = function CardActions({ children }) {
return <div className="card-actions">{children}</div>;
};
Card.Footer = function CardFooter({ children }) {
return <div className="card-footer">{children}</div>;
};
// Usage
function ProductCard({ product }) {
return (
<Card variant="product" size="large">
<Card.Image src={product.image} alt={product.name} />
<Card.Header>
<Card.Title>{product.name}</Card.Title>
<Card.Subtitle>${product.price.toFixed(2)}</Card.Subtitle>
</Card.Header>
<Card.Content>
<p>{product.description}</p>
</Card.Content>
<Card.Actions>
<button>Add to Cart</button>
<button>View Details</button>
</Card.Actions>
<Card.Footer>
<p>{product.stock} in stock</p>
</Card.Footer>
</Card>
);
}
Real-World Example: Ant Design's Form
Many popular UI libraries use composition patterns extensively. For example, Ant Design's Form components:
import { Form, Input, Button, Select, DatePicker } from 'antd';
function RegistrationForm() {
const [form] = Form.useForm();
const onFinish = (values) => {
console.log('Registration values:', values);
};
return (
<Form
form={form}
name="register"
onFinish={onFinish}
layout="vertical"
>
<Form.Item
name="name"
label="Full Name"
rules={[{ required: true, message: 'Please enter your name' }]}
>
<Input placeholder="Enter your full name" />
</Form.Item>
<Form.Item
name="email"
label="Email"
rules={[
{ required: true, message: 'Please enter your email' },
{ type: 'email', message: 'Please enter a valid email' }
]}
>
<Input placeholder="Enter your email" />
</Form.Item>
<Form.Item
name="role"
label="Role"
rules={[{ required: true, message: 'Please select a role' }]}
>
<Select placeholder="Select your role">
<Select.Option value="developer">Developer</Select.Option>
<Select.Option value="designer">Designer</Select.Option>
<Select.Option value="manager">Manager</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="startDate"
label="Start Date"
>
<DatePicker />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Register
</Button>
</Form.Item>
</Form>
);
}
This example shows how composition creates a clean, declarative API while maintaining flexibility and reusability.
Handling Cross-Cutting Concerns
Cross-cutting concerns are aspects of your application that affect multiple components, such as authentication, logging, or error handling. Composition patterns can help manage these concerns elegantly.
Composition for Error Boundaries
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log the error to an error reporting service
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong.</h2>
<p>{this.state.error.toString()}</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<div className="app">
<Header />
<main>
<ErrorBoundary fallback={<p>Dashboard failed to load</p>}>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary fallback={<p>User stats failed to load</p>}>
<UserStats />
</ErrorBoundary>
</main>
<Footer />
</div>
);
}
Context for Authentication
// Create an authentication context
const AuthContext = createContext();
// Authentication provider component
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Check for existing authentication on mount
useEffect(() => {
const checkAuth = async () => {
try {
const token = localStorage.getItem('authToken');
if (token) {
// Validate token with API
const response = await fetch('/api/auth/validate', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
// Invalid token
localStorage.removeItem('authToken');
setUser(null);
}
}
} catch (error) {
console.error('Auth check failed:', error);
setUser(null);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
// Login function
const login = async (email, password) => {
try {
setLoading(true);
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
const { user, token } = await response.json();
// Store token
localStorage.setItem('authToken', token);
// Update state
setUser(user);
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
} finally {
setLoading(false);
}
};
// Logout function
const logout = () => {
localStorage.removeItem('authToken');
setUser(null);
};
const value = {
user,
loading,
isAuthenticated: !!user,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom hook for using the auth context
function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Protected route component
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
const location = useLocation();
if (loading) {
return <div>Loading authentication...</div>;
}
if (!isAuthenticated) {
// Redirect to login with return path
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// Usage in application
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<UserProfile />
</ProtectedRoute>
}
/>
</Routes>
</Router>
</AuthProvider>
);
}
Composition vs. Inheritance for Cross-Cutting Concerns
React favors composition for handling cross-cutting concerns because it:
- Maintains separation of concerns
- Keeps components focused on their primary responsibility
- Allows for flexible and selective application of behaviors
- Provides better runtime flexibility than static inheritance
This approach enables a cleaner architecture where concerns like authentication, error handling, and logging can be applied declaratively just where needed.
Practice Activities
Activity 1: Building a Compound Component
Objective: Practice creating a compound component system.
Instructions:
- Create a
Selectcompound component system with the following parts:Select: Main container componentSelect.Trigger: Button that opens the dropdownSelect.Options: Container for optionsSelect.Option: Individual option item
- Implement shared state using React Context
- Add functionality for selecting an option and showing/hiding the dropdown
Activity 2: Converting a Component to Use Composition
Objective: Practice refactoring components to use composition for greater flexibility.
Instructions:
- Take the following component and refactor it to use composition:
function Alert({
title,
message,
type = 'info',
onClose,
showIcon = true,
actions = []
}) {
return (
<div className={`alert alert-${type}`}>
{showIcon && <span className={`alert-icon icon-${type}`}>{getIcon(type)}</span>}
<div className="alert-content">
{title && <h4 className="alert-title">{title}</h4>}
<p className="alert-message">{message}</p>
</div>
{actions.length > 0 && (
<div className="alert-actions">
{actions.map((action, index) => (
<button
key={index}
className={`alert-action ${action.className || ''}`}
onClick={action.onClick}
>
{action.text}
</button>
))}
</div>
)}
{onClose && (
<button className="alert-close" onClick={onClose}>×</button>
)}
</div>
);
}
// Helper function
function getIcon(type) {
switch(type) {
case 'success': return '✓';
case 'warning': return '⚠';
case 'error': return '✕';
default: return 'ℹ';
}
}
Activity 3: Creating a HOC and Hook Alternative
Objective: Compare different composition patterns for the same functionality.
Instructions:
- Create a higher-order component called
withDarkModethat adds dark mode functionality to a component - Then implement the same functionality as a custom hook called
useDarkMode - Finally, create a context provider approach with
DarkModeProvideranduseDarkModecontext hook - Compare the pros and cons of each approach
Resources for Further Learning
Official Documentation:
Community Resources:
- Compound Components with React Hooks by Kent C. Dodds
- React Patterns at patterns.dev
- React Component Composition by Robin Wieruch
UI Libraries with Strong Composition Patterns:
Summary
- Composition is React's preferred method for building complex UIs from simpler components.
- The
childrenprop is a powerful way to implement containment and create flexible component APIs. - Specialization allows you to create specific versions of components through composition rather than inheritance.
- Compound components use parent-child relationships and shared state to create cohesive component systems.
- Render props provide a way to share code between components by passing render functions as props.
- Higher-order components (HOCs) enhance components by wrapping them with additional functionality.
- Hooks have largely replaced HOCs and render props for sharing behavior, but each pattern has its place.
- Well-designed component composition leads to more maintainable, flexible, and reusable code.
In the next lecture, we'll explore React's state management and lifecycle features in more depth.