Component Single Responsibility

Keep each component focused on one concern (UI, state management, or orchestration—never all three).

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.

Correct Example

// 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.

Incorrect Example

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.

Key Takeaways

  • One job per component: Separate data fetching (containers), stateful logic (hooks), and presentation (UI components).
  • Small components test easier: A render‑only component needs no network mocks; a data hook needs no DOM.
  • Better reuse: Container components can swap in different UIs (mobile vs. desktop) without touching fetch logic.
  • Clear mental model: When each file is focused, new teammates can trace flow quickly instead of parsing a 300‑line blob.