24

Methods for tracking action status in Redux

 4 years ago
source link: https://www.tuicool.com/articles/VvAbmq2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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.

Find Out How LogRocket Tracks

Redux Actions & 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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK