Presentational vs Container Components: Still Relevant in 2025?
If you’ve been building React apps for more than a year, you’ve likely stumbled across the Presentational vs Container component pattern. It’s one of the oldest mental models in the React ecosystem — and while React has evolved significantly since its early days, this separation of concerns still offers architectural clarity, especially in large-scale apps.
So let’s revisit this pattern, explore when it shines, when it breaks, and how it fits in with modern tools like hooks.
The Basics: What Are They?
Presentational Components
These are the “dumb” components. Their sole purpose is to describe how things look. They receive data and callbacks via props and have no knowledge of where the data comes from.
Traits:
- Focus on UI and layout
- Stateless (or only local UI state)
- Reusable and easy to test
- No side effects or data fetching
// src/components/UserCard.jsx
function UserCard({ name, onFollow }) {
return (
<div className="p-4 border rounded">
<h2>{name}</h2>
<button onClick={onFollow}>Follow</button>
</div>
);
}
Container Components
These are the “smart” components. They handle how things work: fetching data, managing state, and implementing logic. Their job is to provide data to presentational components.
Traits:
- Manage data and business logic
- Handle state, effects, and API calls
- Compose and configure presentational components
- Often not reusable
// src/containers/UserCard.jsx
import { followUser } from '../services/userActions';
import UserCard from '../components/UserCard';
function UserCardContainer({ userId }) {
const { data, isLoading, error } = useUser(userId);
const handleFollow = () => {
followUser(userId);
};
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading user</p>;
return <UserCard name={data.name} onFollow={handleFollow} />;
}
Why Use This Pattern?
Benefits
- Separation of concerns: UI and logic live in separate files.
- Reusability: You can reuse presentational components across different contexts.
- Testability: It’s easier to write unit tests for pure presentational components.
- Readability: Your components stay small and focused.
Downsides
- Boilerplate: Extra components just to “connect” things can feel redundant.
- Too rigid: In small apps, this separation might be overkill.
- Modern hooks blur the lines between the two (more on that next).
Hooks Changed Everything
When hooks came around, the distinction between presentational and container components became… fuzzy.
With hooks like useState, useEffect, and custom hooks, any component can manage logic, even the “dumb” ones.
So now we see more of a compositional model:
import { followUser } from '../services/userActions';
import useUser from '../hooks/useUser';
function UserCard({ userId }) {
const { data, isLoading, error } = useUser(userId);
const handleFollow = (userId) => {
followUser(userId);
};
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading user</p>;
return (
<div className="p-4 border rounded">
<h2>{data.name}</h2>
<button onClick={() => handleFollow(userId)}>Follow</button>
</div>
);
}
This isn’t strictly presentational anymore — it fetches data and triggers actions — but it’s self-contained and easier to manage.
How to Use the Pattern in 2025
When You Should Use It:
- You’re building a design system or a component library
- You want to unit test logic separately from UI
- Your team has backend and design-heavy roles that work on different layers
- You’re working on a large-scale app where decoupling logic is essential
When You Don’t Need It:
- In small apps or prototypes
- When using custom hooks to encapsulate logic
- In tightly-coupled UI + logic components (e.g., modals or forms)
Pro Tip: Embrace Custom Hooks Instead
Instead of maintaining an extra layer of container components, move logic into custom hooks. This keeps your component tree shallow while still separating concerns.
import { followUser } from '../services/userActions';
function useUserCard(userId) {
const { data, isLoading, error } = useUser(userId);
const onFollow = () => followUser(userId);
return { data, isLoading, error, onFollow };
}
import useUserCard from '../hooks/useUserCard';
function UserCard({ userId }) {
const { data, isLoading, error, onFollow } = useUserCard(userId);
return (
<div className="p-4 border rounded">
<h2>{data.name}</h2>
<button onClick={() => onFollow(userId)}>Follow</button>
</div>
);
}
Final Thoughts
Presentational and Container components still offer architectural discipline, but they’re no longer a hard rule. Modern React encourages flexible composition via hooks, context, and co-located logic.
Think of the pattern as a guidepost, not a guardrail.
If it helps you write cleaner, more testable code — use it. If it slows you down, evolve it.