Introduction to Component Composition
Component composition is the process of combining smaller, more focused components to build complex user interfaces. It's one of React's fundamental design principles and key to building maintainable applications.
Think of component composition like building with LEGO blocks – each piece has a specific purpose, and by connecting them in various ways, you can create complex structures. Similarly, in React, we compose small, focused components to build sophisticated applications.
Why Component Composition Matters
Composition offers several key advantages over other approaches like inheritance:
- Reusability: Components can be reused in different contexts
- Separation of concerns: Each component handles a specific piece of functionality
- Maintainability: Easier to understand, test, and modify smaller components
- Flexibility: Compose components in different ways to create new features
Real-world analogy: Consider a car manufacturing plant. Rather than building an entire car in one process, different teams create specialized parts (engine, transmission, body panels) that are then assembled. This allows for specialization, quality testing at each stage, and the ability to use the same parts across different car models.
Containment Pattern
The containment pattern allows components to accept and render children, much like HTML elements. This pattern is implemented using the children prop in React.
Example: A Card Component
Let's create a reusable Card component that can wrap any content:
// Card.jsx
function Card({ children, title }) {
return (
<div className="card">
{title && <div className="card-header">{title}</div>}
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<div>
<Card title="User Profile">
<h3>Jane Doe</h3>
<p>Software Engineer</p>
<button>View Profile</button>
</Card>
<Card title="Weather">
<h3>San Francisco</h3>
<p>68°F, Partly Cloudy</p>
</Card>
</div>
);
}
The Card component is responsible for the styling and structure, while the content remains flexible and determined by the parent component.
Real-world application: UI component libraries like Material-UI, Chakra UI, and Ant Design all use the containment pattern extensively for their container components like cards, modals, and layout components.
Specialized Components Pattern
In this pattern, we create a collection of related components that work together, with one primary parent component and specialized child components.
Example: A Tabs Component
// Tabs component family
const TabsContext = React.createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = React.useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs-container">
{children}
</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ children, value }) {
const { activeTab, setActiveTab } = React.useContext(TabsContext);
return (
<button
className={`tab ${activeTab === value ? 'active' : ''}`}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabPanel({ children, value }) {
const { activeTab } = React.useContext(TabsContext);
if (value !== activeTab) return null;
return <div className="tab-panel">{children}</div>;
}
// Make them available as properties of the parent
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanel = TabPanel;
// Usage
function App() {
return (
<Tabs defaultTab="profile">
<Tabs.TabList>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="settings">Settings</Tabs.Tab>
<Tabs.Tab value="notifications">Notifications</Tabs.Tab>
</Tabs.TabList>
<Tabs.TabPanel value="profile">
<h3>User Profile</h3>
{/* Profile content */}
</Tabs.TabPanel>
<Tabs.TabPanel value="settings">
<h3>Settings</h3>
{/* Settings content */}
</Tabs.TabPanel>
<Tabs.TabPanel value="notifications">
<h3>Notifications</h3>
{/* Notifications content */}
</Tabs.TabPanel>
</Tabs>
);
}
This pattern creates a cohesive API for a complex component by defining a clear parent-child relationship between components that are meant to be used together.
Real-world examples: Libraries like Reach UI, Headless UI, and Radix UI use this pattern extensively to create accessible compound components.
Practice Activity
Build a Modal Component with Composition
Create a Modal component that uses the containment pattern to allow flexible content. The Modal should have:
- A backdrop that covers the screen
- A modal container with customizable content
- Open/close functionality
- Optional header and footer sections
Start with this skeleton:
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
}
// Now extend this to support Modal.Header and Modal.Footer components
Challenge: Refactor the Modal to use the specialized components pattern to create a Modal.Header, Modal.Body, and Modal.Footer component family.
Key Takeaways
- Component composition is a fundamental pattern in React for building complex UIs from simpler parts
- The containment pattern uses the children prop to create flexible wrapper components
- The specialized components pattern creates cohesive component families with a clear API
- Composition promotes reusability, maintainability, and separation of concerns
- Well-composed components are easier to test, refactor, and extend