Container / Presentational Pattern
This pattern separates what to render (presentational) from how to get data (container). Also known as smart/dumb or stateful/stateless components.
Presentational component - pure UI
tsx
// only props, no data fetching, no business logic
interface UserCardProps {
name: string;
email: string;
avatar: string;
onFollow: () => void;
}
function UserCard({ name, email, avatar, onFollow }: UserCardProps) {
return (
<div className="card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
<button onClick={onFollow}>Follow</button>
</div>
);
}Container component - data and logic
tsx
function UserCardContainer({ userId }: { userId: string }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => api.getUser(userId),
});
const followMutation = useMutation({
mutationFn: () => api.followUser(userId),
});
if (isLoading) return <Skeleton />;
return (
<UserCard
name={user.name}
email={user.email}
avatar={user.avatar}
onFollow={() => followMutation.mutate()}
/>
);
}Benefits
UserCardis easy to test - pure function of props.UserCardis easy to reuse with different data sources.UserCardis easy to work on in Storybook.- Logic in
UserCardContaineris isolated and replaceable.
Modern take
With hooks this separation is less strict - logic can live in custom hooks inside the same component. But the principle of separating UI from data concerns remains valid.
tsx
// hooks replace the container component
function UserCard({ userId }: { userId: string }) {
const { user, follow } = useUserCard(userId); // all logic in hook
return <UserCardUI user={user} onFollow={follow} />;
}This pattern is less about file structure and more about keeping UI components free of side effects. A
UserCardthat knows nothing about APIs is far easier to test and maintain.
updated
did you know it?