Written by Maciej Cieślar✏️
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.
The usual asynchronous action flow
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.
The simplest solution for storing the pending indicator
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.
Each action on its own
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.
Creating a separate reducer
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.
Creating the reducer
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,
error: null
};
}
if (action.type === GET_USERS_FAILURE) {
return {
...state,
user: null,
error: action.payload.error
};
}
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:
Summary
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.
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post Methods for tracking action status in Redux appeared first on LogRocket Blog.
Top comments (0)