React is a popular JavaScript library for building user interfaces. As applications grow in complexity, developers adopt design patterns to manage state and UI logic effectively. Two such patterns are the Container/Presenter pattern and Compound Components. Understanding these patterns helps create more maintainable and reusable React components.
Container/Presenter Pattern
The Container/Presenter pattern separates concerns within components. The container component manages state and logic, while the presenter component focuses solely on rendering UI based on props. This separation makes components easier to test, reuse, and maintain.
How It Works
The container component handles data fetching, state management, and event handling. It passes necessary data and callbacks as props to the presentational component, which renders the UI. This division allows developers to modify presentation or logic independently.
Example
Consider a user profile feature. The container fetches user data and manages loading states, while the presenter displays the user information.
function UserContainer() {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetchUserData().then(data => {
setUser(data);
setLoading(false);
});
}, []);
if (loading) {
return <p>Loading...</p>;
}
return <UserPresenter user={user} />;
}
function UserPresenter({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
Compound Components
Compound Components allow related components to work together seamlessly by sharing implicit state or context. They enable developers to create flexible, declarative APIs that are easy to compose and extend.
How It Works
Instead of passing props explicitly, compound components use React's Context API to share state. The parent component provides context, while child components consume it, allowing for more natural composition.
Example
Imagine a custom Tabs component where TabList and TabPanel are separate components but work together through context.
const TabsContext = React.createContext();
function Tabs({ children, defaultIndex = 0 }) {
const [selectedIndex, setSelectedIndex] = React.useState(defaultIndex);
return (
<TabsContext.Provider value={{ selectedIndex, setSelectedIndex }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ index, children }) {
const { selectedIndex, setSelectedIndex } = React.useContext(TabsContext);
const isSelected = index === selectedIndex;
return (
<button
className={isSelected ? 'active' : ''}
onClick={() => setSelectedIndex(index)}
>
{children}
</button>
);
}
function TabPanel({ index, children }) {
const { selectedIndex } = React.useContext(TabsContext);
return selectedIndex === index ? <div className="tab-panel">{children}</div> : null;
}
// Usage
<Tabs>
<TabList>
<Tab index={0}>Tab 1</Tab>
<Tab index={1}>Tab 2</Tab>
</TabList>
<TabPanel index={0}>Content for Tab 1</TabPanel>
<TabPanel index={1}>Content for Tab 2</TabPanel>
</Tabs>
Choosing the Right Pattern
Both patterns improve component organization but serve different purposes. Use the Container/Presenter pattern when you want clear separation of concerns, especially for complex data handling. Choose Compound Components when building flexible, composable UI elements that share state or context.
Summary
Understanding React patterns like Container/Presenter and Compound Components enhances your ability to build scalable, maintainable applications. They promote code reuse, improve readability, and facilitate testing, making your development process more efficient.