Single Responsibility Principle in React
Image Credit: GeeksforGeeks

Single Responsibility Principle in React

The Single Responsibility Principle (SRP) is a design pattern based on the idea that a module, class or function should have only responsibility. As a result, “every module, class or function should have one reason to change”. In this article, I am going to explain this design pattern in relation to how it applies to React applications.

In React it means that a component should have one responsibility, and only one reason to change. I will use a simple code example that violates this principle and correct it one step at a time. Consider the following React component that is supposed to fetch and display posts on the page:

import { useState, useEffect } from "react";
import axios from "axios";

export type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

const PostsPage = () => {
  const [posts, setPosts] = useState<Post[] | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchPosts = async() => {
    setIsLoading(true)
      try {
   	const response = await axios.get("https://jsonplaceholder.typicode.com/posts/");
   	setPosts(response.data);
   	setError("")
      } catch(error) {
   	if (error.response.data) {
   	  setError(error.response.data)
   	} else {
   	  setError("Something went wrong. Please try again later.")
   	}
      } finally {
   	setIsLoading(false);
      }
     }
     fetchPosts();
    }, []);

    if (isLoading) return <div>Loading ....</div>;

    if (error) return <div>{error}</div>;

    if (!posts) return <div>No posts yet. Please check back later.</div>;

    return (
      <div>
   	<h1>Posts</h1>
   	<div>
   	  {posts.map((post) => (
   	    <div key={post.id}>
   	      <h2>{post.title}</h2>
   	      <p>{post.body}</p>
   	    </div>
   	    ))}
   	 </div>
      </div>
    );
}        

This component violates the SRP. Why is that so?

Judging from the name of the component, it is the page component that is supposed to combine all components related to the posts. Looking at the code above, the PostsPage component has more than one responsibility; fetching data, rendering the loading and the error component as well as the posts and single post components.?

The code above will work perfectly fine, but will lead to challenges later as the code grows. We cannot make this code reusable for other components. The component has a lot of logic that we would have a hard time fixing errors or maintaining it. When the codebase grows, it will become hard to understand what is going on in this component.

Imagine you are a new developer joining a team with this sort of logic with a codebase of about 3 thousand lines of code all congested into one component. Remember this is just a simplified version of this component. You may need to fetch the users (authors) associated with each post and display them along their posts, or you may also want to add post liking and commenting functionalities. Imagine how big this component will be already.

To fix this, we have to first figure out what needs to be removed from this component. I would first remove the logic of rendering a single post into its own component, say PostCard. This will make the postcard to be reusable in different places, for instance displaying a user’s posts on their profile page.

import { Post } from "./PostsPage";

export const PostCard = ({ post }: { post: Post}) => {
  return (
    <div>
	<h2>{post.title}</h2>
	<p>{post.body}</p>
    </div>
  );
};        

The next thing we want to remove is the logic of rendering a list of posts from the PostsPage component. Why? It is not the responsibility of this component to map through all the posts. What if, for example, we want to first sort the posts, say by title? The component will grow and have more than one reason to change. So, let’s extract that logic into its own component, PostsList.

import { PostCard } from "./PostCard";
import { Post } from "./PostsPage";

export const PostsList = ({ posts }: { posts: Post[]}) => {
  return (
	<div>
	    <h1>Posts</h1>
	    <div>
		{posts.map((post) => (
		    <PostCard posts={post} />
		))}
	    </div>
	</div>
  );
};        

The codebase is starting to look simpler and understandable now, but we are not done yet. The loading component can be removed from the PostsPage into its own component, say Loader. It looks too simple here because we are only displaying one div, but imagine it is a complex spinner that may sometimes change the text below it to keep the user engaged while the posts are loading.

export const Loader = () => {
    return <div>Loading...</div>;
}        

The error component also can be extracted into its separate component as well as the no posts component, but I will not do it here. I want to address the elephant in the room, the posts fetching logic. It would be good to extract this logic into a custom hook. Imagine if we want to handle some more logic like attaching request interceptors, or adding the request cancellation logic. I will create a separate custom hook called useFetchPosts.

import { useState, useEffect } from "react";
import axios from "axios";
import { Post } from "./PostsPage";

export const useFetchPosts = () => {
	const [posts, setPosts] = useState<Post[] | null>(null);
	const [isLoading, setIsLoading] = useState(false);
	const [error, setError] = useState("");

	useEffect(() => {
	    const fetchPosts = async() => {
		setIsLoading(true)
		try {
		    const response = await axios.get("https://jsonplaceholder.typicode.com/posts/");
		    setPosts(response.data);
		    setError("")
		} catch(error) {
		    if (error.response.data) {
		        setError(error.response.data)
		    } else {
			 setError("Something went wrong. Please try again later.")
		    }
		} finally {
		    setIsLoading(false);
		}
	    }
	    fetchPosts();
	}, []);

    return { posts, isLoading, error };
};        

This is how our main PostsPage component would look like now:

import { useFetchPosts } from "./useFetchPosts";
import { PostsList } from "./PostsList";
import { Loader } from "./Loader";

export type Post = {
	userId: number;
	id: number;
	title: string;
	body: string;
};

const PostPage = () => {
	const { posts, isLoading, error } = useFetchPosts();

	if (isLoading) return <Loader />;

	// this can have its own component
	if (error) return <div>{error}</div>;

	// can be extracted into its own component too
	if (!posts) return <div>No posts yet. Please check back later.</div>;

	return (
		<div>
			<PostsList posts={posts} />
		</div>
	);
}        

Now we've separated each functionality in our program. We can use the components anywhere we want to use them in our code. The examples we used have no styles or other complicated logic – this is mainly for simplicity. You can have as complex functionality as you want but they should be linked to the responsibility of the component.

With the logic separated, our code is easier to understand as each core functionality has its own component. We can test for errors more efficiently. Also, the code is now reusable. Before, we could only use these functionalities inside one component, but now they can be used in any other component that requires them.

The code is also easily maintainable and scalable because instead of reading interconnected lines of code, we have separated concerns so we can focus on the features we want to work on.

要查看或添加评论,请登录

Pritchard Mambambo的更多文章

社区洞察

其他会员也浏览了