Skip to main content

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:

  1. Reusability: Build components once and use them in multiple places
  2. Maintainability: Smaller components are easier to understand and modify
  3. Separation of concerns: Each component handles a specific piece of functionality
  4. Testability: Focused components are easier to test in isolation
  5. 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:

jsx
// 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:

jsx
// 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:

jsx
// 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:

  1. Adding more props to customize a component's behavior
  2. Using composition to enable customization

Let's compare these approaches with a simple alert component:

Approach 1: Props for Configuration

jsx
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

jsx
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.

jsx
// 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:

jsx
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:

jsx
// 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:

jsx
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

  1. Keep components focused: Each component should do one thing well
  2. Design with composition in mind: Make components that are easily composable
  3. Use children prop liberally: It's the simplest form of composition
  4. Consider specialized sub-components: Create component "families" that work well together
  5. Document component relationships: Make it clear how components are meant to be used together
  6. Consider what should be configurable: Balance between props and composition

Common Composition Mistakes

  1. Prop drilling: Passing props through many component layers. Consider context or composition instead.
  2. Overly specific components: Creating components that aren't reusable. Look for patterns to generalize.
  3. Too many props: If a component has too many props, consider breaking it into multiple components.
  4. 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

  1. Convert a component with many boolean props (like isVisible, hasHeader, etc.) to use composition instead.
  2. Create a compound component system for form elements (similar to Formik).
  3. Implement a flexible Card component that allows for different header, body, and footer content via composition.
  4. Build a navigation system with nested menus using component composition.
  5. 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! :)