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…
Javascript
import React, {useEffect, useReducer, useState} from "react" ; const initialState = { isLoading: true }; // COMPLEX STATE MANAGEMENT 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) } // REMOTE DATA FETCHING useEffect(() => { dispatch({type: 'LOADING' }) .then(response => response.json()) .then(json => { dispatch({type: 'FINISHED' }) setUsers(json) }) },[]) // PROCESSING DATA useEffect(() => { const filteredUsers = users.map(user => { return { id: user.id, name: user.name, contact: `${user.phone} , ${user.email}` }; }); setFilteredUsers(filteredUsers) },[users]) // COMPLEX UI RENDERING return <> <div> Users List</div> <div> Loading state: {state.isLoading? 'Loading' : 'Success' }</div> {users.map(user => { return <div key={user.id} onClick={() => showDetails(user.id)}> <div>{user.name}</div> <div>{user.email}</div> </div> })} </> } |
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…
Javascript
import {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…
Javascript
import 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 <> <div> Users List</div> <div> Loading state: {isLoading? 'Loading' : 'Success' }</div> {filteredUsers.map(user => { return <div key={user.id} onClick={() => showDetails(user.id)}> <div>{user.name}</div> <div>{user.email}</div> </div> })} </> } |
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 Reusable
useGetRemoteData is serving two purposes in your code…
- Fetching data from a remote source
- Filtering data
We 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.
Javascript
import {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…
Javascript
export 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…
Javascript
import {useEffect, useState} from "react" ; import {useHttpGetRequest} from "./useHttpGet" ; const REMOTE_URL = 'https://jsonplaceholder.typicode.com/users' 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 Components
Separate the code of user details into a different component which is only responsible to display the UserDetails.
Javascript
const UserDetails = (user) => { const showDetails = (user) => { alert(user.contact) } return <div key={user.id} onClick={() => showDetails(user)}> <div>{user.name}</div> <div>{user.email}</div> </div> } |
Now the original component looks like below:
Javascript
import React from "react" ; import {useGetRemoteData} from "./useGetRemoteData" ; export const Users = () => { const {filteredUsers , isLoading} = useGetRemoteData() return <> <div> Users List</div> <div> Loading state: {isLoading? 'Loading' : 'Success' }</div> {filteredUsers.map(user => <UserDetails user={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 user
- useGetRemoteData.js: Responsible for filtering remote data
- useHttpGetrequest.js: Responsible for HTTP calls
- LoadingReducer.js: Complex state management.
Hope things are clear to you now.