Hey there 👋🏽,
Firstly, sorry I couldn't come up with a more catchy name 😩. Believe me, I tried.
We know that the boilerplate for Redux can be a little bit, yunno, annoying... And if you've got lots of states to manage, for maybe a huge awesome app you're working on, using Redux can mean you having to write so much code for not-so-much tasks.
In this article, I'm going to show you an easier and more generic way I discovered to handle ERROR, SUCCESS and LOADING states in React for all the actions you need in Redux. So you can reduce the code in your reducer (pun intended), and know more about how redux does its stuff.
Let's Dive into it!!!

So basically, I'll be using Redux with hooks, so all the useSelector and useDispatch stuff... So you may need to have some experience with Redux to follow along.
If you understand Redux but you don't really understand Redux with Hooks, no worries, you can still keep reading, then you can check out this video later.
Also, if you wanna know how react itself works with hooks, check out this one
Now let's assume I have two states that I want to manage in my app: user's details and user's posts
So I guess my state would probably look like this:
const initialState={
userDetails: {},
posts: []
}
Now the actions for userDetails
Normally, the user should only be able to fetch and edit his/her details, so we'd be having GET and PATCH Requests
This is how actions/userDetails.js would look like:
import Axios from "axios";
import { MY_BASE_API_URL } from "../../utils/url";
export const fetchUserDetails = () => (dispatch) => {
Axios.get(MY_BASE_API_URL + "/user/user-profile/")
.then((response) => {
dispatch({ type: "FETCH_USER_DETAILS", payload: response.data });
})
.catch((error) => {
console.log(error.response)
});
};
export const updateUserDetails = (editedDetails) => (dispatch) => {
Axios.patch(
MY_BASE_API_URL + "/user/user-profile/" + editedDetails.id + "/", editedDetails )
.then((response) => {
dispatch({ type: "UPDATE_USER_DETAILS", payload: response.data });
})
.catch((error) => {
console.log(error.response)
});
};
Next, the actions for the posts
Lets assume the user can create, fetch, edit and delete posts, so we'd be having GET, POST, PATCH and DELETE Requests
This is how actions/userPosts.js would look like:
// userPosts.js
import Axios from "axios";
import { MY_BASE_API_URL } from "../../utils/url";
export const fetchUserPosts = () => (dispatch) => {
Axios.get(MY_BASE_API_URL + "/posts/")
.then((response) => {
dispatch({ type: "FETCH_USER_POSTS", payload: repsonse.data });
})
.catch((error) => {
console.log(error.response)
});
};
export const createNewPost = (newPostPayload) => (dispatch) => {
Axios.post(MY_BASE_API_URL + "/posts/", newPostPayload)
.then((response) => {
dispatch({ type: "CREATE_NEW_POST", payload: response.data });
})
.catch((error) => {
console.log(error);
});
};
export const updateUserPost = (editedPost) => (dispatch) => {
Axios.put(MY_BASE_API_URL + "/posts/" + editedPost.id + "/", editedPost )
.then((response) => {
dispatch({ type: "UPDATE_USER_POST", payload: response.data });
})
.catch((error) => {
console.log(error.response)
});
};
export const deleteUserPost = (postToDelete) => (dispatch) => {
Axios.delete(MY_BASE_API_URL + "/posts/" + postToDelete.id + "/")
.then((response) => {
dispatch({ type: "DELETE_USER_POST", payload: response.data });
})
.catch((error) => {
console.log(error.response)
});
};
All good! Now how do we access their loading states?
The whole trick is in an extra state value called loadingActions

So here's how it works:
loadingActions is an array that holds all the actions that are being loaded. Once an action is called, it adds (or removes) a string with the name of the action that is loading, depending on the loading value(true or false)... to the loadingActions array.
Let us give it a look
We're going to add a loading action to fetchUserDetails:
export const fetchUserDetails = () => (dispatch) => {
dispatch({
type: "LOADING",
isLoading: true,
loadingType: "FETCH_USER_DETAILS",
});
Axios.get(MY_BASE_API_URL + "/user/user-profile/")
.then((response) => {
dispatch({
type: "LOADING",
isLoading: false,
loadingType: "FETCH_USER_DETAILS",
});
dispatch({ type: "FETCH_USER_DETAILS", payload: response.data });
})
.catch((error) => {
dispatch({
type: "LOADING",
isLoading: false,
loadingType: "FETCH_USER_DETAILS",
});
console.log(error.response)
});
};
Now let's see how the reducer handles this...
Here's our reducers/commonReducer.js
const initialState = {
loadingActions: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case "LOADING":
if (action.isLoading === true) {
return {
...state,
loadingActions: [...state.loadingActions, action.loadingType],
};
} else {
return {
...state,
loadingActions: state.loadingActions.filter(
(eachAction) => eachAction !== action.loadingType
),
};
}
}
}
So during the fetchUserDetails phase, the value of loadingActions moves from
[] (before fetch is called) ==> ['FETCH_USER_DETAILS'] (during fetch) ==> [] (after fetch)
Okay! Let's see how that works
We'll create a page called details.js where we'll render the user's details:
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchUserDetails } from "../../redux/actions/userDetails.js";
export default function Details(){
const loadingActions = useSelector((state) => state.common.loadingActions );
const userDetails= useSelector((state) => state.details.userDetails);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchUserDetails());
}, []);
return(
<div>
//Now we check if "FETCH_USER_DETAILS" exisits in our loadingActions
{loadingActions.includes("FETCH_USER_DETAILS") ?
<p>Loading......</p>
:
<div>
<h3>{userDetails.name}</h3>
<p>{userDetails.email}</p>
</div>
}
</div>
)
}
The big selling point of this method is that you can use one reducer and state value to handle the loading states for all your data.
So if we wanted to get the loading state for getUserPosts, all we need to do is change 'FETCH_USER_DETAILS' to 'FETCH_USER_POSTS' and check if
loadingActions.includes("FETCH_USER_POSTS")
See?

Now let's take a look at how that can work for error states...
If there was an error during createNewPost, here's what we can do:
export const createNewPost = (newPostPayload) => (dispatch) => {
dispatch({
type: "LOADING",
isLoading: true,
loadingType: "CREATE_NEW_POST",
});
Axios.post(MY_BASE_API_URL + "/posts/", newPostPayload)
.then((response) => {
dispatch({
type: "LOADING",
isLoading: false,
loadingType: "CREATE_NEW_POST",
});
dispatch({ type: "CREATE_NEW_POST", payload: response.data });
})
.catch((error) => {
dispatch({
type: "LOADING",
isLoading: false,
loadingType: "CREATE_NEW_POST",
});
dispatch({
type: "ERROR",
hasError: true,
errorMessage: error.response.data,
errorType: "CREATE_NEW_POST",
});
console.log(error);
});
};
So basically what we're doing here is similar to that for LOADING. That means we would also have a state value called errorActions initialized as []
NOTE: The errorActions would be an array of objects, rather than strings used loadingActions. This is because the error object will store:
- The action of the error (errorType), and
- The error message (errorMessage)
So we can update our reducers/commonReducer.js like this:
const initialState = {
loadingActions: [],
errorActions: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case "LOADING":
if (action.isLoading === true) {
return {
...state,
loadingActions: [...state.loadingActions, action.loadingType],
};
} else {
return {
...state,
loadingActions: state.loadingActions.filter(
(eachAction) => eachAction !== action.loadingType
),
};
}
case "ERROR":
if (action.hasError === true) {
return {
...state,
errorActions: [
...state.actionsError,
{
errorType: action.errorType,
errorMessage: action.errorMessage,
},
],
};
} else {
return {
...state,
errorActions: state.errorActions.filter(
(eachError) => eachError.errorType !== action.errorType
),
};
}
}
}
Note how an object is added to the errorActions array while strings are added to the loadingActions array. There really is no rule, I just made it this way in order to accommodate for the errorMessage

In order to remove stale data for the next action call, we have to remove the error object for that instance. Don't worry, javascript is still going to know that there was an error initially. So it's just a form of 'clean-up'
So the createNewPost action will look like this:
export const createNewPost = (newPostPayload) => (dispatch) => {
dispatch({
type: "LOADING",
isLoading: true,
loadingType: "CREATE_NEW_POST",
});
Axios.post(MY_BASE_API_URL + "/posts/", newPostPayload)
.then((response) => {
dispatch({
type: "LOADING",
isLoading: false,
loadingType: "CREATE_NEW_POST",
});
dispatch({ type: "CREATE_NEW_POST", payload: response.data });
})
.catch((error) => {
dispatch({
type: "LOADING",
isLoading: false,
loadingType: "CREATE_NEW_POST",
});
dispatch({
type: "ERROR",
hasError: true,
errorMessage: error.response.data,
errorType: "CREATE_NEW_POST",
});
dispatch({
type: "ERROR",
hasError: false,
errorType: "CREATE_NEW_POST",
});
console.log(error);
});
};
Alright! Now how do we use this in our rendering?
Let's look at a component where the user can submit a post
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { createNewPost } from "../../redux/actions/userDetails.js";
export default function CreatePosts(){
const errorActions= useSelector((state) => state.common.errorActions);
const dispatch = useDispatch();
useEffect(() => {
if(actionsError.find((error) => error[errorType] === "CREATE_NEW_POST")){
alert('An error occured!!!')
}
}, [errorActions]);
const onSubmit=()=>{
const newPostPayload={
title: 'Some title',
body: 'Some body'
}
dispatch(createNewPost(newPostPayload))
}
return(
<div>
<button onClick={()=>onSubmit()} >Click me!</button>
</div>
)
}
Aaand that's it!
This same method can also be used for update and delete actions just like they were used in fetch and post.
You can also use it to get states such as successActions, loadedActions, etc.
Sorry if the code formatting looks weird 😬, still getting the hang of it...

You can always reach out to me in my email or GitHub or Twitter.
Oh oh, check out my website too at adedaniel.netlify.app
PS: This is my very first article, so please leave a like and drop a comment so I can answer any question you've got...
Thank you for reading! You're awesome ✨