Single Responsibility Principle: One Component, One Reason to Change
Split bloated components into focused pieces and extract logic with a custom hook.
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 stateUserProfile— render user details onlyUserActions— handle UI actions onlyUserDashboard— 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
- Extract data fetching to
useUserand return{ user, loading }. - Create
UserProfileto render only name, email, and role. - Create
UserActionsto render buttons and handle click events. - Refactor
UserDashboardto composeUserProfileandUserActions. - 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} />