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.
