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!