Back to all notes

Single Responsibility Principle: One Component, One Reason to Change

Split bloated components into focused pieces and extract logic with a custom hook.

3 min read

Introduction

The Single Responsibility Principle (SRP) says a module should have one reason to change. In React, that means each component should focus on a single job: fetching data, rendering UI, or handling actions—but not all three at once.

Why this matters

When a component fetches data, renders UI, and handles actions, it becomes hard to test and reason about. Small changes ripple through unrelated logic. The Single Responsibility Principle (SRP) says a component should do one job well and have one reason to change.

The problem

A common anti‑pattern: a UserDashboard component that:

  • Fetches user data
  • Renders profile details
  • Implements edit/delete actions

Everything is coupled inside one file and one component.

Inefficient approach

A single component owns fetching, rendering, and actions—hard to test and easy to break:

import { useState, useEffect } from "react";

function UserDashboard() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setTimeout(() => {
      setUser({ name: "John Doe", email: "john@example.com", role: "Developer" });
      setLoading(false);
    }, 1000);
  }, []);

  const handleEdit = () => alert(`Edit user ${user.name} clicked`);
  const handleDelete = () => alert(`Delete user ${user.name} clicked`);

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h2>User Dashboard</h2>
      <div>
        <h3>{user.name}</h3>
        <p>Email: {user.email}</p>
        <p>Role: {user.role}</p>
      </div>
      <div>
        <button onClick={handleEdit}>Edit User</button>
        <button onClick={handleDelete}>Delete User</button>
      </div>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Single Responsibility Principle</h1>
      <UserDashboard />
    </div>
  );
}

export default App;

The solution

Split responsibilities into:

  • useUser — fetch and expose user data and loading state
  • UserProfile — render user details only
  • UserActions — handle UI actions only
  • UserDashboard — compose them
import { useState, useEffect } from "react";

function useUser() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setTimeout(() => {
      setUser({ name: "John Doe", email: "john@example.com", role: "Developer" });
      setLoading(false);
    }, 1000);
  }, []);

  return { user, loading };
}

function UserProfile({ user }) {
  return (
    <div>
      <h3>{user.name}</h3>
      <p>Email: {user.email}</p>
      <p>Role: {user.role}</p>
    </div>
  );
}

function UserActions({ user }) {
  const handleEdit = () => alert(`Edit user ${user.name} clicked`);
  const handleDelete = () => alert(`Delete user ${user.name} clicked`);
  return (
    <div>
      <button onClick={handleEdit}>Edit User</button>
      <button onClick={handleDelete}>Delete User</button>
    </div>
  );
}

function UserDashboard() {
  const { user, loading } = useUser();
  if (loading) return <div>Loading...</div>;
  return (
    <div>
      <h2>User Dashboard</h2>
      <UserProfile user={user} />
      <UserActions user={user} />
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Single Responsibility Principle</h1>
      <UserDashboard />
    </div>
  );
}

export default App;

Step-by-step

  1. Extract data fetching to useUser and return { user, loading }.
  2. Create UserProfile to render only name, email, and role.
  3. Create UserActions to render buttons and handle click events.
  4. Refactor UserDashboard to compose UserProfile and UserActions.
  5. Verify loading is handled in one place and UI pieces are isolated.

Why this is better

  • Components are easier to test in isolation.
  • UI, data, and actions evolve independently.
  • The dashboard composes features instead of owning everything.

Tips from practice

  • Start by extracting a hook when data fetching, memoization, or derived state grows.
  • Keep presentational components pure—props in, UI out.
  • Prefer composition over props that leak internal details.

SRP makes change sets smaller and your intent clearer. It’s the foundation for scalable component architecture.

Knowledge base

Problem snippet:

useEffect(() => { /* fetch inside UI */ }, []);
<button onClick={handleEdit}>Edit User</button>

Solution snippet:

function useUser() { /* fetch here, return { user, loading } */ }
<UserProfile user={user} />
<UserActions user={user} />