- GBlog, ReactJS, Web Technologies

How To Use Single Responsibility Principle in ReactJS?

If you’re a developer then surely you might have listened to SOLID Principles word several times in your programming career. In Software development, the SOLID principle works as a guideline for developers. It doesn’t matter which language you’re using in your project, to make your code clean and maintainable, you need to apply the SOLID principle in your project. SOLID principle makes the task easier for developers, and also it helps them in maintaining the code in their project. Let’s talk about React now, a very popular framework among developers. With the help of React, you can create a beautiful UI. In the earlier stage of your career, you may do a lot of mistakes in writing the code in React but once you will have experience in working on it, you will understand that it’s also important to write clean and maintainable code in React. For this reason, surely one thing that can help you are the SOLID principle. You can write small, beautiful, and clean React components. SOLID principle makes your component visible with clear responsibilities. The SOLID principle tells us that each class should have a single purpose of existence. In React, components should do only one thing at a time.  Now let’s understand how to refactor a bad code in React and make it cleaner. First, let’s consider a bad example…Javascriptimport React, {useEffect, useReducer, useState} from “react”;  const initialState = {    isLoading: true};  function reducer(state, action) {    switch (action.type) {        case ‘LOADING’:            return {isLoading: true};        case ‘FINISHED’:            return {isLoading: false};        default:            return state;    }}  export const SingleResponsibilityPrinciple = () => {      const [users , setUsers] = useState([])    const [filteredUsers , setFilteredUsers] = useState([])    const [state, dispatch] = useReducer(reducer, initialState);      const showDetails = (userId) => {        const user = filteredUsers.find(user => user.id===userId);        alert(user.contact)    }          useEffect(() => {        dispatch({type:’LOADING’})            .then(response => response.json())            .then(json => {                dispatch({type:’FINISHED’})                setUsers(json)            })    },[])          useEffect(() => {        const filteredUsers = users.map(user => {            return {                id: user.id,                name: user.name,                contact: `${user.phone} , ${user.email}`            };        });        setFilteredUsers(filteredUsers)    },[users])          return          Users List         Loading state: {state.isLoading? ‘Loading’: ‘Success’}        {users.map(user => {            return showDetails(user.id)}>                {user.name}                {user.email}                    })}    }Here, we are fetching the data from the remote source, and then we are rendering it in the UI. We are also detecting the loading state of the API call. Basically, the above code is divided into mainly…four things…Remote data fetching…Data filtering…Complex state management…Complex UI functionality…Now let’s see how to improve the design of this code and how to make it more cleanable…1. Separating Data Processing Logic From the Code.You should never keep your HTTP calls inside the component. This is a basic rule of thumb. To remove these codes from the component, you can follow several strategies. You can create a custom hook, and you can move your data fetching and filtering logic inside that custom hook. Lets’ see how to do this…Create a hook named useGetRemoteData. It looks like below…Javascriptimport {useEffect, useReducer, useState} from “react”;  const initialState = {    isLoading: true};  function reducer(state, action) {    switch (action.type) {        case ‘LOADING’:            return {isLoading: true};        case ‘FINISHED’:            return {isLoading: false};        default:            return state;    }}  export const useGetRemoteData = (url) => {      const [users , setUsers] = useState([])    const [state, dispatch] = useReducer(reducer, initialState);      const [filteredUsers , setFilteredUsers] = useState([])        useEffect(() => {        dispatch({type:’LOADING’})            .then(response => response.json())            .then(json => {                dispatch({type:’FINISHED’})                setUsers(json)            })    },[])      useEffect(() => {        const filteredUsers = users.map(user => {            return {                id: user.id,                name: user.name,                contact: `${user.phone} , ${user.email}`            };        });        setFilteredUsers(filteredUsers)    },[users])      return {filteredUsers , isLoading: state.isLoading}}Now if you look at your main component then it will look like this…Javascriptimport React from “react”;import {useGetRemoteData} from “./useGetRemoteData”;  export const SingleResponsibilityPrinciple = () => {      const {filteredUsers , isLoading} = useGetRemoteData()      const showDetails = (userId) => {        const user = filteredUsers.find(user => user.id===userId);        alert(user.contact)    }      return          Users List         Loading state: {isLoading? ‘Loading’: ‘Success’}        {filteredUsers.map(user => {            return showDetails(user.id)}>                {user.name}                {user.email}                    })}    }You can observe that your component is much cleaner and easier to understand now. Let’s make our code much better using some more techniques or methods.2. Separate the Code of Data Fetching to Make it ReusableuseGetRemoteData is serving two purposes in your code…Fetching data from a remote sourceFiltering dataWe can make a separate hook, and we can move our data fetching logic there. Let’s give it the name…useHttpGetRequest. It takes the URL as a component.Javascriptimport {useEffect, useReducer, useState} from “react”;import {loadingReducer} from “./LoadingReducer”;  const initialState = {    isLoading: true};  export const useHttpGetRequest = (URL) => {      const [users , setUsers] = useState([])    const [state, dispatch] = useReducer(loadingReducer, initialState);      useEffect(() => {        dispatch({type:’LOADING’})        fetch(URL)            .then(response => response.json())            .then(json => {                dispatch({type:’FINISHED’})                setUsers(json)            })    },[])      return {users , isLoading: state.isLoading}  }Let’s also separate the reducer logic into a separate file… Javascriptexport function loadingReducer(state, action) {    switch (action.type) {        case ‘LOADING’:            return {isLoading: true};        case ‘FINISHED’:            return {isLoading: false};        default:            return state;    }}After performing the above two operations…useGetRemoteData looks like below… Javascriptimport {useEffect, useState} from “react”;import {useHttpGetRequest} from “./useHttpGet”;  export const useGetRemoteData = () => {    const {users , isLoading} = useHttpGetRequest(REMOTE_URL)    const [filteredUsers , setFilteredUsers] = useState([])      useEffect(() => {        const filteredUsers = users.map(user => {            return {                id: user.id,                name: user.name,                contact: `${user.phone} , ${user.email}`            };        });        setFilteredUsers(filteredUsers)    },[users])      return {filteredUsers , isLoading}}Now you can observe that the code becomes much cleaner. We can perform some more operations and make this code much better. Let’s see that how to do this…3. Decompose UI ComponentsSeparate the code of user details into a different component which is only responsible to display the UserDetails.Javascriptconst UserDetails = (user) => {      const showDetails = (user) => {        alert(user.contact)    }      return showDetails(user)}>        {user.name}        {user.email}    }Now the original component looks like below:Javascriptimport React from “react”;import {useGetRemoteData} from “./useGetRemoteData”;  export const Users = () => {    const {filteredUsers , isLoading} = useGetRemoteData()      return          Users List         Loading state: {isLoading? ‘Loading’: ‘Success’}        {filteredUsers.map(user => )}    }Did you observe that how your code which was too long is now too short? We just decomposed the code into five separate components, and we put our logic over there. Each component is now responsible for single responsibility.Let’s review our code and see what we did here. We created five different components…Users.js: Responsible for displaying the user list.UserDetails.js: Responsible for displaying details of a useruseGetRemoteData.js: Responsible for filtering remote datauseHttpGetrequest.js: Responsible for HTTP callsLoadingReducer.js: Complex state management.Hope things are clear to you now.