React Component Composition
Introduction
Component composition is a powerful pattern in React that allows you to build complex user interfaces by combining smaller, reusable components. Rather than creating large, monolithic components with numerous responsibilities, composition encourages you to create focused components that you can arrange together like building blocks.
This approach aligns with React's philosophy of creating reusable UI elements that can be combined in various ways. By mastering component composition, you'll write more maintainable code, reduce duplication, and create more flexible UI components.
Why Use Component Composition?
Before diving into implementation, let's understand why component composition is essential:
- Reusability: Build components once and use them in multiple places
- Maintainability: Smaller components are easier to understand and modify
- Separation of concerns: Each component handles a specific piece of functionality
- Testability: Focused components are easier to test in isolation
- Flexibility: Compose components in different ways to create varied UIs
Basic Component Composition
At its simplest, component composition involves nesting one component inside another. Let's start with a basic example:
// Button.jsx
function Button({ children, onClick }) {
return (
<button
onClick={onClick}
className="primary-button"
>
{children}
</button>
);
}
// App.jsx
function App() {
return (
<div>
<Button onClick={() => console.log('Clicked!')}>
Click Me
</Button>
</div>
);
}
In this example, we've created a reusable Button
component that accepts children and an onClick
handler. The key aspect here is the use of children
prop, which is a special prop automatically passed to components that allows a component to render whatever is passed between its opening and closing tags.
Using the children Prop
The children
prop is fundamental to component composition. It enables components to be containers for other elements:
// Card.jsx
function Card({ children, title }) {
return (
<div className="card">
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<Card title="Welcome">
<p>This is a card component with some content.</p>
<Button onClick={() => alert('Hello!')}>Say Hello</Button>
</Card>
);
}
Here, the Card
component serves as a container that provides consistent styling and structure, while the content inside it can vary. This is a powerful pattern for creating reusable layout components.
Specialized Components
Sometimes we need to create components with specialized sub-components that work together. For example, a Form
component might have specific FormField
components designed to work within it:
// Form components
function Form({ children, onSubmit }) {
return (
<form onSubmit={onSubmit}>
{children}
<div className="form-actions">
<button type="submit">Submit</button>
</div>
</form>
);
}
function FormField({ label, children }) {
return (
<div className="form-field">
<label>{label}</label>
<div className="form-input">
{children}
</div>
</div>
);
}
// Usage
function SignupForm() {
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted');
};
return (
<Form onSubmit={handleSubmit}>
<FormField label="Name">
<input type="text" name="name" />
</FormField>
<FormField label="Email">
<input type="email" name="email" />
</FormField>
<FormField label="Password">
<input type="password" name="password" />
</FormField>
</Form>
);
}
This pattern creates a clean API for forms while enabling flexibility in what each form field contains.
Composition vs. Props for Customization
When building components, you'll often need to decide between:
- Adding more props to customize a component's behavior
- Using composition to enable customization
Let's compare these approaches with a simple alert component:
Approach 1: Props for Configuration
function Alert({ message, type, icon, dismissible }) {
return (
<div className={`alert alert-${type}`}>
{icon && <span className="alert-icon">{icon}</span>}
<span className="alert-message">{message}</span>
{dismissible && <button className="alert-dismiss">×</button>}
</div>
);
}
// Usage
function App() {
return (
<Alert
message="Operation successful!"
type="success"
icon="✓"
dismissible={true}
/>
);
}
Approach 2: Composition for Configuration
function Alert({ children, type }) {
return (
<div className={`alert alert-${type}`}>
{children}
</div>
);
}
function AlertIcon({ icon }) {
return <span className="alert-icon">{icon}</span>;
}
function AlertMessage({ children }) {
return <span className="alert-message">{children}</span>;
}
function AlertDismiss({ onDismiss }) {
return <button className="alert-dismiss" onClick={onDismiss}>×</button>;
}
// Usage
function App() {
return (
<Alert type="success">
<AlertIcon icon="✓" />
<AlertMessage>Operation successful!</AlertMessage>
<AlertDismiss onDismiss={() => console.log('dismissed')} />
</Alert>
);
}
The composition approach is more verbose but provides greater flexibility. The props approach is more concise but less adaptable to unforeseen use cases.
Advanced Composition Patterns
Compound Components
Compound components are sets of components that work together to provide a cohesive functionality. Think of them like a <select>
and <option>
in HTML.
// Tabs system using compound components
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = React.useState(defaultTab);
// Clone children and inject activeTab state
const childrenWithProps = React.Children.map(children, child => {
return React.cloneElement(child, { activeTab, setActiveTab });
});
return <div className="tabs-container">{childrenWithProps}</div>;
}
function TabList({ children, activeTab, setActiveTab }) {
// Clone tab children to inject active state and click handlers
const tabs = React.Children.map(children, child => {
return React.cloneElement(child, {
active: child.props.id === activeTab,
onClick: () => setActiveTab(child.props.id)
});
});
return <div className="tab-list">{tabs}</div>;
}
function Tab({ id, active, onClick, children }) {
return (
<button
className={`tab ${active ? 'active' : ''}`}
onClick={onClick}
>
{children}
</button>
);
}
function TabPanels({ children, activeTab }) {
// Only render the active panel
const activePanel = React.Children.toArray(children).find(
child => child.props.id === activeTab
);
return <div className="tab-panels">{activePanel}</div>;
}
function TabPanel({ id, children }) {
return <div className="tab-panel">{children}</div>;
}
// Usage
function App() {
return (
<Tabs defaultTab="tab1">
<TabList>
<Tab id="tab1">Profile</Tab>
<Tab id="tab2">Settings</Tab>
<Tab id="tab3">Notifications</Tab>
</TabList>
<TabPanels>
<TabPanel id="tab1">
<h2>Profile Content</h2>
<p>User profile information goes here</p>
</TabPanel>
<TabPanel id="tab2">
<h2>Settings Content</h2>
<p>User settings go here</p>
</TabPanel>
<TabPanel id="tab3">
<h2>Notifications Content</h2>
<p>User notifications go here</p>
</TabPanel>
</TabPanels>
</Tabs>
);
}
This pattern provides a clean, intuitive API while handling complex state internally.
Render Props
The render props pattern allows for more specialized control over what and how children get rendered:
function DataFetcher({ url, render }) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
setLoading(true);
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return render({ data, loading, error });
}
// Usage
function UserProfile({ userId }) {
return (
<DataFetcher
url={`https://api.example.com/users/${userId}`}
render={({ data, loading, error }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error loading user data.</p>;
return (
<div className="user-profile">
<h2>{data.name}</h2>
<p>Email: {data.email}</p>
</div>
);
}}
/>
);
}
The render props pattern allows for great flexibility in how data is rendered, while separating concerns between data fetching and presentation.
Higher Order Components (HOC)
While less common in modern React, Higher Order Components are another composition pattern worth knowing:
// HOC to add theme support
function withTheme(Component) {
return function ThemedComponent(props) {
const theme = useContext(ThemeContext);
return <Component {...props} theme={theme} />;
};
}
// Using the HOC
function Button({ theme, children }) {
return (
<button
style={{
backgroundColor: theme.primary,
color: theme.text
}}
>
{children}
</button>
);
}
const ThemedButton = withTheme(Button);
// Usage
function App() {
return <ThemedButton>Click Me</ThemedButton>;
}
Real-World Example: Building a Dashboard UI
Let's combine these patterns to build a simple dashboard UI:
function Dashboard() {
return (
<Layout>
<Header>
<Logo />
<Navigation>
<NavItem link="/dashboard">Dashboard</NavItem>
<NavItem link="/analytics">Analytics</NavItem>
<NavItem link="/settings">Settings</NavItem>
</Navigation>
<UserMenu />
</Header>
<SideBar>
<SideBarSection title="Quick Actions">
<ActionButton icon="add">New Project</ActionButton>
<ActionButton icon="chat">Messages</ActionButton>
<ActionButton icon="notification">Alerts</ActionButton>
</SideBarSection>
<SideBarSection title="Recent">
<RecentItem title="Annual Report" time="2 hours ago" />
<RecentItem title="Q3 Analytics" time="Yesterday" />
</SideBarSection>
</SideBar>
<MainContent>
<Card title="Overview">
<DataFetcher
url="/api/stats"
render={({data, loading}) => (
loading ? <Spinner /> : <StatsDisplay stats={data} />
)}
/>
</Card>
<CardGrid>
<Card title="Recent Sales">
<SalesChart />
</Card>
<Card title="Top Products">
<ProductList />
</Card>
<Card title="Team Performance">
<TeamStats />
</Card>
</CardGrid>
</MainContent>
<Footer>
<Copyright />
<FooterLinks />
</Footer>
</Layout>
);
}
This example demonstrates how component composition lets you create a complex UI with a clean, readable structure. Each component handles a specific part of the UI, making the code more maintainable.
Best Practices for Component Composition
- Keep components focused: Each component should do one thing well
- Design with composition in mind: Make components that are easily composable
- Use children prop liberally: It's the simplest form of composition
- Consider specialized sub-components: Create component "families" that work well together
- Document component relationships: Make it clear how components are meant to be used together
- Consider what should be configurable: Balance between props and composition
Common Composition Mistakes
- Prop drilling: Passing props through many component layers. Consider context or composition instead.
- Overly specific components: Creating components that aren't reusable. Look for patterns to generalize.
- Too many props: If a component has too many props, consider breaking it into multiple components.
- Insufficient abstraction: Not identifying common patterns that could be extracted into reusable components.
Summary
Component composition is a fundamental pattern in React that allows you to:
- Build complex UIs from simpler, reusable components
- Create flexible component APIs that can adapt to various use cases
- Separate concerns between components
- Write more maintainable and testable code
By mastering component composition techniques like children props, compound components, and render props, you'll be able to create more elegant, maintainable React applications.
Additional Resources
Exercises
- Convert a component with many boolean props (like
isVisible
,hasHeader
, etc.) to use composition instead. - Create a compound component system for form elements (similar to Formik).
- Implement a flexible Card component that allows for different header, body, and footer content via composition.
- Build a navigation system with nested menus using component composition.
- Refactor an existing component in your project to use the render props pattern for more flexibility.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)