Understanding the Single Responsibility Principle in React
Table of contents
The Single Responsibility Principle (SRP) in React emphasizes that each component should have one, and only one, reason to change. This means a component should focus on a single piece of functionality or responsibility. By adhering to SRP, you ensure that your components are more modular, easier to understand, and simpler to maintain. This approach helps in creating a scalable and maintainable codebase, as each component is focused on a specific task and can be developed, tested, and debugged independently.
Here’s an example of a bad component where everything (fetching posts, mapping over them, and rendering individual posts) is handled inside a single component, which violates the Single Responsibility Principle (SRP):
Bad Example: All-in-One Component
import React, { useState, useEffect } from 'react';
function AllInOneComponent() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchPosts = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
setPosts(data);
} catch (err) {
setError('Failed to fetch posts');
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
return (
<div>
<h1>Posts</h1>
{loading ? (
<p>Loading...</p>
) : error ? (
<p>{error}</p>
) : (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
)}
</div>
);
}
export default AllInOneComponent;
What's Wrong with this All-In-One Component?
State Management Inside the Component:
The component directly manages the state for posts, loading, and error, causing unnecessary re-renders and breaking the Single Responsibility Principle (SRP).
States like posts, loading, and error should be handled in a custom hook for better separation of concerns and reusability across components..
API Fetch Logic Inside the Component:
The
fetchPosts
function, which calls the API, is inside the component, making it harder to test and maintain.The fetch logic should be in a custom hook to keep the component clean, reusable, and easier to test.
Directly Mapping Over Posts in the Same Component:
The posts are mapped and rendered directly within the same component, reducing reusability and mixing rendering logic with data fetching and state management.
Instead, the component should pass the posts to a dedicated PostList component for rendering, with each post handled by a PostCard component.
Good Example: Refactoring with SRP
To follow SRP, we need to break down the responsibilities and structure our code into smaller, reusable components. We'll create a custom hook to handle the fetching logic and separate the rendering logic into PostList
and PostCard
components.
Step 1: Create a Custom Hook for Fetching Posts
We start by moving the API fetch logic into a custom hook, which allows us to reuse the fetching logic across different components.
import { useState, useEffect } from 'react';
// Custom hook to fetch posts
export function useFetchPosts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
setPosts(data);
} catch (err) {
setError('Failed to fetch posts');
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
return { posts, loading, error };
}
- What's good here?: The fetch logic is now encapsulated in a reusable custom hook. The component doesn’t have to deal with managing the fetch, error, or loading states, which keeps it clean and focused on UI rendering.
Step 2: PostCard Component
Next, we’ll create a simple component that is responsible for rendering each individual post.
import React from 'react';
// PostCard component for displaying a single post
function PostCard({ post }) {
return (
<li>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
);
}
export default PostCard;
- What's good here?:
PostCard
only handles rendering a single post. It takes apost
as a prop and is reusable for any other components needing to render a post, following the SRP.
Step 3: PostList Component
This component is responsible for mapping over the fetched posts and rendering each post with PostCard
.
import React from 'react';
import PostCard from './PostCard';
// PostList component for displaying a list of posts
function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</ul>
);
}
export default PostList;
- What's good here?:
PostList
only focuses on mapping over the posts and rendering each post throughPostCard
. This component doesn’t manage any state or fetch logic, keeping it simple and reusable.
Step 4: Main Component Using the Custom Hook
Finally, the main component that uses the custom hook useFetchPosts
to fetch the data and then passes the fetched posts to the PostList
component for rendering.
import React from 'react';
import { useFetchPosts } from './useFetchPosts';
import PostList from './PostList';
function PostPage() {
const { posts, loading, error } = useFetchPosts();
if (loading) return <p>Loading...</p>;
if (error) return <p>{error}</p>;
return (
<div>
<h1>Posts</h1>
<PostList posts={posts} />
</div>
);
}
export default PostPage;
- What's good here?: The
PostPage
component does only one thing: fetching and rendering posts. The logic for fetching the posts is cleanly separated into theuseFetchPosts
custom hook, and the rendering logic is delegated to thePostList
andPostCard
components, following the SRP perfectly.
Why is This Better?
Separation of Concerns: Each component has a single responsibility.
PostPage
is responsible for fetching the posts,PostList
for rendering the list of posts, andPostCard
for rendering an individual post.Reusability: The
useFetchPosts
hook can now be reused anywhere we need to fetch posts, and thePostCard
andPostList
components can be reused across the app.Testability: Testing becomes easier since the logic is broken into smaller, isolated parts. We can test the fetch logic, rendering logic, and component state separately.
Scalability: As the app grows, keeping the code modular and following the SRP makes maintaining and extending the app much easier.
Conclusion
In conclusion, following the Single Responsibility Principle (SRP) in React development is key to a clean, modular, and maintainable codebase. By breaking components into smaller units, each with one responsibility, we improve reusability, testability, and scalability. Using custom hooks for data fetching and separating rendering logic into dedicated components shows the benefits of SRP. This approach simplifies development and makes each component easier to understand, debug, and extend, resulting in a more robust and efficient React application.