Single Responsibility Principle: One Component, One Reason to Change
Split bloated components into focused pieces and extract logic with a custom hook.
The Single Responsibility Principle says a module should have one reason to change, which in React I read as one job per component, fetching data, rendering UI, or handling actions. The component doing all three at once is the one I dread opening, because small changes ripple through unrelated logic and it’s hard to test.
UserDashboard does all three jobs
The anti-pattern I keep writing is a UserDashboard that fetches the user, renders the profile details, and implements the edit and delete actions, everything coupled in one file.
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;
Four pieces, one job each
I split it into useUser for fetching and exposing user and loading, UserProfile for the details, UserActions for the buttons, and a thin UserDashboard that composes 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;
The extraction is mechanical. useUser returns { user, loading }. UserProfile renders name, email, and role. UserActions renders the buttons and handles their clicks. UserDashboard pulls from the hook and composes the two, with loading handled in exactly one place.
Every piece now tests in isolation. UI, data, and actions evolve independently. The dashboard composes features instead of owning everything, and change sets get smaller.
Condensed, the smell and the fix.
useEffect(() => { /* fetch inside UI */ }, []);
<button onClick={handleEdit}>Edit User</button>
function useUser() { /* fetch here, return { user, loading } */ }
<UserProfile user={user} />
<UserActions user={user} />
How I pull these apart in practice
I extract a hook first, once fetching, memoization, or derived state starts growing. Presentational components stay pure, props in, UI out. And I prefer composition over props that leak internal details.