Why? Improves reuse, testability, and cognitive load.
A “kitchen‑sink” component that fetches data, owns state, and renders UI feels convenient—until you need to reuse the UI elsewhere, write unit tests, or tweak the data flow. Bloated components turn every small change into a refactor risk and hide the mental model of the app. By giving each component a single job, you keep files short, tests targeted, and future changes isolated.
// UserListContainer.tsx
import { useEffect, useState } from "react";
import UserList from "./UserList";
type User = { id: number; name: string; avatar: string };
export default function UserListContainer() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Loading…</p>;
return <UserList users={users} />;
}
// UserList.tsx — purely presentational
type User = { id: number; name: string; avatar: string };
export default function UserList({ users }: { users: User[] }) {
return (
<ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{users.map((u) => (
<li
key={u.id}
className="flex items-center gap-3 rounded-xl p-4 shadow"
>
<img src={u.avatar} alt={u.name} className="h-10 w-10 rounded-full" />
<span className="font-medium">{u.name}</span>
</li>
))}
</ul>
);
}
What’s happening?
UserListContainer
handles data retrieval and loading state.UserList
focuses solely on rendering the list.
Either piece can now be reused, tested, or swapped independently.function UserListMonolith() {
// 👉 Data fetching
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState(""); // 👉 UI state
const [selected, setSelected] = useState<User | null>(null);
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Loading…</p>;
// 👉 Rendering + orchestration all mashed together
return (
<>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
className="mb-4 border p-2"
placeholder="Search users"
/>
<ul>
{users
.filter((u) => u.name.toLowerCase().includes(search.toLowerCase()))
.map((u) => (
<li key={u.id} onClick={() => setSelected(u)}>
{u.name}
</li>
))}
</ul>
{selected && (
<div className="fixed inset-0 grid place-content-center bg-black/40">
<div className="rounded bg-white p-6">
<h2 className="text-lg font-semibold">{selected.name}</h2>
{/* more UI */}
<button onClick={() => setSelected(null)}>Close</button>
</div>
</div>
)}
</>
);
}
// One file handles fetching, filtering, modal logic, and UI.
// Reusing the list or the modal elsewhere now means copy‑pasting.