Having worked on a fair share of React and Redux applications, I can’t help but notice that many people have a hard time indicating to the user that a given action is currently taking place.
Let’s consider the following example:
class RegisterForm extends React.Component { state = { isLoading: false } async handleSubmit(event) { event.preventDefault(); this.setState({ isLoading: true, }); try { const result = await post('localhost:8000/api/users', { data: {}, }); } catch (error) { // do something with the error } // do something with the result this.setState({ isLoading: false }); } render() { return ( <form onSubmit={this.handleSubmit.bind(this)} > <input type="text" /> <button type="submit">Submit</button> {this.state.isLoading && <p>Spinner!</p>} </form> ); } }
Here we have a simplified React register form that should display a loading indicator — say, a spinner — once the user has hit the submit button. Well, we could simply make the request inside the component and use setState
to keep track of its status, and that would work just fine.
This solution has two problems, however. First, the request and its logic are defined inside a component; we would need to repeat this very same code should we want the same functionality elsewhere in our application.
Second, what if we wanted to display the spinner outside the component? How would we go about lifting that component’s state a few components up?
Here is where Redux comes to our aid.
By having an immutable global state available everywhere in our app, we can save the action’s status inside the state and have it available anywhere — thus, the indicator can be displayed anywhere. Let’s take a look at the usual asynchronous flow of actions in Redux.
Actions in Redux are objects and, as such, are dispatched synchronously. But thanks to various middleware, we can dispatch them in an asynchronous manner.
There are many libraries that allow us to dispatch actions asynchronously — redux-thunk, redux-saga, and redux-observable, to name a few.
The usual flow goes like this: first, we dispatch the action that is supposed to set things in motion (usually the action’s type ends with a _REQUEST
suffix, e.g., GET_USER_REQUEST
).
Then, somewhere in our state, we make a note that the action is pending, like this:
{ isLoading: true }
Or:
{ pending: true }
Note: I prefer the name pending because it doesn’t imply that the action is necessarily loading something.
Then, once the action is finished, we dispatch one of the following actions, depending on the outcome: GET_USER_SUCCESS
or GET_USER_FAILURE
.
Both of these actions will set the pending
value to false
and save (somewhere in the state) either the error or the result.
One common approach to handling the loading states of actions is to create a state of the following shape:
{ user: { isLoading: true, user: { ... } token: '...' } }
We can see here that we have a user section where we store all the user-related data.
This solution works well only for the most basic applications, and here’s why: What does isLoading
tell us, exactly? There are many actions that may be considered user-related, such as registering, logging in, and updating; with this solution, we have no way of differentiating between them.
A better approach to handling actions’ pending states is to create a separate object for each action we have.
Here’s an example:
{ user: { register: { pending: false, error: null, }, login: { pending: false, error: null, }, } }
This way, we can track a given action’s state throughout the whole application or identify specific actions as they occur. This allows us to display the register
action’s state in multiple places in the application.
While a state like this is much more manageable, this solution still needs a lot of boilerplate code to be written for each action. Let’s consider a different approach, where we create a separate reducer for the pending indicators.
In Redux, each dispatched action executes all the reducers, regardless of whether a given reducer is even supposed to handle it.
By creating a separate reducer dedicated to keeping the pending states, we can use the SUCCESS
and FAILURE
actions to save the errors and results in other parts of the state.
Since the reducer will be executed on every action, we should filter out those we are not interested in: actions whose type doesn’t end with _REQUEST
, _SUCCESS
, or _FAILURE
.
Since our convention is to name actions like GET_USERS_REQUEST
, we can create a function called getActionName
, in which we split the name at the _
character, remove the last part (REQUEST
, SUCCESS
, or FAILURE
), and join the remaining parts with _
.
function getActionName(actionType) { if (typeof actionType !== 'string') { return null; } return actionType .split("_") .slice(0, -1) .join("_"); }
If actionType
is something other than a string, like a commonly used Symbol
, we return null
to avoid an error.
This way, we turn GET_USERS_REQUEST
into GET_USERS
and thus have a name under which we can save the pending state in the state.
Here’s the code for the reducer:
const pendingReducer = (state = {}, action) => { const { type } = action; const actionName = getActionName(type); if (!actionName) { return { ...state, } } if (type.endsWith("_REQUEST")) { return { ...state, [actionName]: { pending: true } }; } if (type.endsWith("_SUCCESS") || type.endsWith("_FAILURE")) { return { ...state, [actionName]: { pending: false } }; } return { ...state }; };
First, we check whether the action’s type ends with _REQUEST
. If that is indeed the case, we create a new entry in the state with the action’s name as a key and { pending: true }
as a value.
Then, if the action’s type ends with _SUCCESS
or _FAILURE
, we do the same thing, but this time we set { pending: false }
as a value.
Now, should we want a user reducer, we can create it like so:
const userReducer = (state = initialUserState, action) => { if (action.type === GET_USERS_SUCCESS) { return { ...state, user: action.payload.user, }; } if (action.type === GET_USERS_FAILURE) { return { ...state, user: null, }; } return { ...state }; };
Now we need not worry about setting pending: true
on each action and then setting it back to false
on success/failure.
Note: We don’t have error handling here, but it could also be done in a separate reducer.
Here’s a live demo for you to play with:
Assigning each action its own state to keep track of status is a scalable solution that relies on a lot of boilerplate code. By creating a separate reducer to handle the logic of managing status, we can reduce the amount of redundant code, but in turn, we lose the flexibility to define some additional fields needed to more accurately track a specific action’s status.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMaking carousels can be time-consuming, but it doesn’t have to be. Learn how to use React Snap Carousel to simplify the process.
Consider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
One Reply to "Methods for tracking action status in Redux"
it does not harm the performance?
Now all your components will subscribed to pendingReducer state, and react will run the diffing algorithm for each of the subscribed components.