Simplifying Asynchronous Action Flow in Redux Applications

The Problem with Traditional Approaches

When building a React register form, displaying a loading indicator once the user submits it can be a challenge. One solution is to make the request inside the component and use setState to track its status. However, this approach has two significant drawbacks:

  • The request logic is tied to the component, making it difficult to reuse elsewhere in the application.
  • If we want to display the spinner outside the component, we’d need to lift the component’s state several levels up.

The Power of Redux

This is where Redux comes to the rescue. By having an immutable global state available everywhere in our app, we can save the action’s status inside the state and make it accessible anywhere. This enables us to display the indicator anywhere in the application.

Asynchronous Action Flow in Redux

In Redux, actions are objects that can be dispatched synchronously or asynchronously using middleware like redux-thunk, redux-saga, or redux-observable. The typical flow involves dispatching an action that sets things in motion (e.g., GETUSERREQUEST), followed by updating the state to reflect the action’s pending status. Once the action is finished, we dispatch a success or failure action (e.g., GETUSERSUCCESS or GETUSERFAILURE), which updates the state accordingly.

const getUserRequest = () => ({ type: 'GETUSERREQUEST' });
const getUserSuccess = (user) => ({ type: 'GETUSERSUCCESS', user });
const getUserFailure = (error) => ({ type: 'GETUSERFAILURE', error });

A Better Approach to Handling Pending States

One common approach is to create a state with a simple isLoading flag. However, this solution has limitations, as it doesn’t differentiate between various user-related actions. A more scalable solution is to create separate objects for each action, allowing us to track individual action states throughout the application.

const initialState = {
  getUser: { isLoading: false, error: null, user: null },
  //...
};

Creating a Separate Reducer for Pending Indicators

By creating a dedicated reducer for pending indicators, we can use SUCCESS and FAILURE actions to save errors and results in other parts of the state. This approach eliminates the need for boilerplate code and provides a more manageable solution.

const pendingReducer = (state = {}, action) => {
  if (!['_REQUEST', '_SUCCESS', '_FAILURE'].some(suffix => action.type.endsWith(suffix))) {
    return state;
  }

  const actionName = getActionName(action.type);
  const isLoading = action.type.endsWith('_REQUEST');
  const error = action.type.endsWith('_FAILURE')? action.error : null;
  const result = action.type.endsWith('_SUCCESS')? action.result : null;

  return {...state, [actionName]: { isLoading, error, result } };
};

In this reducer, we use a getActionName function to extract the action name from the type, and update the state accordingly.

const getActionName = (type) => type.replace(/_REQUEST|_SUCCESS|_FAILURE$/, '');

Try out the live demo to see this approach in action!

Leave a Reply