Skip to main content

Violating Single Responsibility Principle (SRP)

ยท 3 min read
Pere Pages
Software Engineer

This is the first of a series of posts about the most common mistakes in Frontend development. This is the most common mistake I see in React projects.

This post explains what the Single Responsibility Principle (SRP) is and why it's important for React components. It shows how putting too much code and too many tasks in one component can lead to messy, hard-to-maintain apps.


The Mistakeโ€‹

Creating "god components" that handle data fetching, business logic, state management, and UI rendering all in one place.

โŒ Bad: Component doing everything

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

useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
return fetch(`/api/posts/${data.id}`);
})
.then(res => res.json())
.then(posts => {
setPosts(posts.filter(p => p.published));
setLoading(false);
})
.catch(err => setError(err));
}, []);

const handleDelete = (postId) => {
fetch(`/api/posts/${postId}`, { method: 'DELETE' })
.then(() => setPosts(posts.filter(p => p.id !== postId)));
};

if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;

return (
<div className="dashboard">
<header>
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.email}</p>
</header>
<section>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<button onClick={() => handleDelete(post.id)}>Delete</button>
</article>
))}
</section>
</div>
);
}

The Solutionโ€‹

Separate concerns into custom hooks, presentational components, and business logic utilities.

โœ… Good: Separated concerns

API & Data Fetching Layerโ€‹

Components and hooks that call external endpoints:

// Custom hook for data fetching
function useUserWithPosts(userId?: string) {
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
fetchUserWithPosts()
.then(data => {
setUser(data.user);
setPosts(data.posts);
})
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);

return { user, posts, loading, error };
}
// Custom hook for post operations
function usePostOperations() {
const deletePost = async (postId: string) => {
await fetch(`/api/posts/${postId}`, { method: 'DELETE' });
};

return { deletePost };
}

Business Logicโ€‹

Pure functions with no side effects or external dependencies:

function filterPublishedPosts(posts: Post[]): Post[] {
return posts.filter(post => post.published);
}

Presentational Componentsโ€‹

Pure UI components that receive data and callbacks as props:

function UserHeader({ user }: { user: User }) {
return (
<header>
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.email}</p>
</header>
);
}
function PostList({ posts, onDelete }: { posts: Post[]; onDelete: (id: string) => void }) {
return (
<section>
{posts.map(post => (
<PostCard key={post.id} post={post} onDelete={onDelete} />
))}
</section>
);
}

Container Componentโ€‹

Orchestrates everything: fetches data, applies business logic, and wires components together:

function UserDashboard() {
const { user, posts, loading, error } = useUserWithPosts();
const { deletePost } = usePostOperations();

if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
if (!user) return null;

const publishedPosts = filterPublishedPosts(posts);

return (
<div className="dashboard">
<UserHeader user={user} />
<PostList posts={publishedPosts} onDelete={deletePost} />
</div>
);
}

Benefits: Each piece has a single, clear purpose. Testing becomes trivial. Components are reusable and maintainable.