DEV Community

Cover image for Simplifying request/success/failure model for async action in Redux for large applications
Sunil Chaudhary
Sunil Chaudhary

Posted on • Edited on

2

Simplifying request/success/failure model for async action in Redux for large applications

Now that we have learnt the pattern to skillfully handle the async actions in redux, let's dive deeper into how to simplify the same to make code more cleaner and scalable. For those who missed the Part 1, please read here.

Why to simplify?

Notice that, we have written a lot of boilerplate code just to handle one API call. Also, code will become repetitive in nature for multiple calls which contradicts with DRY and other software writing methodologies.

Process of simplification

We will pick each of our actions, types, reducer(s) and simplify them one by one.

Actions and Types

This is how we write our actions and types in this approach

const GET_USERS_LIST_REQUEST = 'GET_USERS_LIST_REQUEST'
const GET_USERS_LIST_SUCCESS = 'GET_USERS_LIST_SUCCESS'
const GET_USERS_LIST_FAILURE = 'GET_USERS_LIST_FAILURE'
const getUsersListRequest = (data) => ({
type: GET_USERS_LIST_REQUEST,
data
})
const getUsersListSuccess = (data) => ({
type: GET_USERS_LIST_SUCCESS,
data
})
const getUsersListFailure = (data) => ({
type: GET_USERS_LIST_FAILURE,
data
})

Observe here that there are 3 actions and 3 types. And the same pattern will be repeated for each API call. Imagine if there are 10 API calls. It means there will be 30 actions and 30 types to be manually written. To avoid this, we will write a helper function that will take one input string and return all the these. The function will look something like this:

/*
Use this helper function to generate actions and types automatically
This will return a object generating 3 actions and 3 types:
{
FAILURE: "GET_USERS_LIST_FAILURE",
SUCCESS: "GET_USERS_LIST_SUCCESS",
REQUEST: "GET_USERS_LIST_REQUEST",
failure: payload => ({ type, payload, }), // type: GET_USERS_LIST_FAILURE
success: payload => ({ type, payload, }), // type: GET_USERS_LIST_SUCCESS
request: payload => ({ type, payload, }), // type: GET_USERS_LIST_REQUEST
}
*/
const actionCreator = action => {
const values = ['SUCCESS', 'FAILURE', 'REQUEST'];
const types = values.reduce((acc, value) => {
const type = `${action}_${value}`;
acc[value] = type;
acc[value.toLowerCase()] = data => ({
type,
data,
});
return acc;
}, {});
return types;
};

If we use the above function then the entire logic for actions and types will be reduced to just one single line

const getUsersList = actionCreator('GET_USERS_LIST')
Enter fullscreen mode Exit fullscreen mode

This will give all the required actions and types.
Next question is how to use all these. The answer is pretty simple. getUsersList is an object, so the relevant actions and types will be following:

  • getUsersList.request instead of getUsersListRequest
  • getUsersList.success instead of getUsersListSuccess
  • getUsersList.failure instead of getUsersListFailure
  • getUsersList.REQUEST instead of GET_USERS_LIST_REQUEST
  • getUsersList.SUCCESS instead of GET_USERS_LIST_SUCCESS
  • getUsersList.FAILURE instead of GET_USERS_LIST_FAILURE

Reducer

This is how current reducer looks like and it is only usable for one api call.

const initialState = {
isLoading: false,
loaded: false,
data: null,
error: null
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'GET_USERS_LIST_REQUEST':
return {
...state,
isLoading: true,
loaded: false,
date: null,
error: null,
}
case 'GET_USERS_LIST_SUCCESS':
return {
...state,
isLoading: false,
loaded: true,
date: action.data,
error: null,
}
case 'GET_USERS_LIST_FAILURE':
return {
...state,
isLoading: false,
loaded: true,
date: null,
error: action.data,
}
default:
return state
}
}

The reducer will now be modified to this

const initialAsyncState = {
isLoading: false,
loaded: false,
data: null,
error: null
}
const initialState = {
usersList: initialAsyncState,
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case getUsersList.REQUEST:
case getUsersList.SUCCESS:
case getUsersList.FAILURE:
return {
...state,
usersList: reducerHandler(state, action, getUsersList)
}
default:
return state
}
}

Note we have done 2 things here:

  • We have clubbed the relevant switch cases together and transferred the logic to update store to a new reducerHandler function.
  • We have added a key usersList which will contain entire state for that particular API. This will make sure that same reducer can be used for multiple API calls.

Let see the definition of reducerHandler function (helper function to be written only once) now:

const reducerHandler = (state, action, actionHandler) => {
switch(action.type) {
case actionHandler.REQUEST:
return {
...state,
isLoading: true,
}
case actionHandler.SUCCESS:
return {
...state,
isLoading: false,
loaded: true,
data: action.data,
error: null
}
case actionHandler.FAILURE:
return {
...state,
isLoading: false,
loaded: true,
error: action.data,
data: null,
}
default:
return state;
}
}

Though we have added few functions which might seem that code is increased but observe that the task of creating multiple actions, types and reducers is reduced to few lines. Now if we have to do a new API call, just one line is added to create actions & types and few lines added in reducers instead of writing about 50 lines and adding a new reducer. This simplifies the code a lot.

Integrating with middlewares

The other part of calling from sagas and dispatching actions will remain same. Let's see an example:

// Sample Component Code
const UsersList = () => {
return (
<Spin showLoader={isLoading}>
<List data={data} />
<Button onClick={getUsersList.request}>Get List</Button>
</Spin>
)
}
// Sample Saga Code
function* getUsersList() {
try {
const response = yield call(api.getUsersList);
yield put(getUsersList.success(response));
} catch (error) {
yield put(getUsersList.failure(error));
}
}
function* saga() {
yield takeEvery(getUsersList.REQUEST, getUsersList);
}

This will help to keep code clean and help developers to increase productivity and focus on other areas.

Exhaustive example

Just to show, how this approach works here is an example with just 3 API calls:

Old approach (read more here)

// Combine all reducers here
export default combineReducers({
userProfile,
userAddress,
userFriends
});
// Types, Actions and Reducer for API 1
const GET_USER_PROFILE_REQUEST = 'GET_USER_PROFILE_REQUEST'
const GET_USER_PROFILE_SUCCESS = 'GET_USER_PROFILE_SUCCESS'
const GET_USER_PROFILE_FAILURE = 'GET_USER_PROFILE_FAILURE'
const getUserProfileRequest = (data) => ({
type: GET_USER_PROFILE_REQUEST,
data
})
const getUserProfileSuccess = (data) => ({
type: GET_USER_PROFILE_SUCCESS,
data
})
const getUserProfileFailure = (data) => ({
type: GET_USER_PROFILE_FAILURE,
data
})
const initialState = {
isLoading: false,
loaded: false,
data: null,
error: null
}
const userProfileReducer = (state = initialState, action) => {
switch (action.type) {
case GET_USER_PROFILE_REQUEST:
return {
...state,
isLoading: true,
loaded: false,
date: null,
error: null,
}
case GET_USER_PROFILE_SUCCESS:
return {
...state,
isLoading: false,
loaded: true,
date: action.data,
error: null,
}
case GET_USER_PROFILE_FAILURE:
return {
...state,
isLoading: false,
loaded: true,
date: null,
error: action.data,
}
default:
return state
}
}
// Types, Actions and Reducer for API 2
const GET_USER_ADDRESS_REQUEST = 'GET_USER_ADDRESS_REQUEST'
const GET_USER_ADDRESS_SUCCESS = 'GET_USER_ADDRESS_SUCCESS'
const GET_USER_ADDRESS_FAILURE = 'GET_USER_ADDRESS_FAILURE'
const getUserFriendsRequest = (data) => ({
type: GET_USER_ADDRESS_REQUEST,
data
})
const getUserFriendsSuccess = (data) => ({
type: GET_USER_ADDRESS_SUCCESS,
data
})
const getUserFriendsFailure = (data) => ({
type: GET_USER_ADDRESS_FAILURE,
data
})
const initialState = {
isLoading: false,
loaded: false,
data: null,
error: null
}
const userAddressReducer = (state = initialState, action) => {
switch (action.type) {
case GET_USER_ADDRESS_REQUEST:
return {
...state,
isLoading: true,
loaded: false,
date: null,
error: null,
}
case GET_USER_ADDRESS_SUCCESS:
return {
...state,
isLoading: false,
loaded: true,
date: action.data,
error: null,
}
case GET_USER_ADDRESS_FAILURE:
return {
...state,
isLoading: false,
loaded: true,
date: null,
error: action.data,
}
default:
return state
}
}
// Types, Actions and Reducer for API 3
const GET_USER_FRIENDS_REQUEST = 'GET_USER_FRIENDS_REQUEST'
const GET_USER_FRIENDS_SUCCESS = 'GET_USER_FRIENDS_SUCCESS'
const GET_USER_FRIENDS_FAILURE = 'GET_USER_FRIENDS_FAILURE'
const getUserAddressRequest = (data) => ({
type: GET_USER_FRIENDS_REQUEST,
data
})
const getUserAddressSuccess = (data) => ({
type: GET_USER_FRIENDS_SUCCESS,
data
})
const getUserAddressFailure = (data) => ({
type: GET_USER_FRIENDS_FAILURE,
data
})
const initialState = {
isLoading: false,
loaded: false,
data: null,
error: null
}
const userFriendsReducer = (state = initialState, action) => {
switch (action.type) {
case GET_USER_FRIENDS_REQUEST:
return {
...state,
isLoading: true,
loaded: false,
date: null,
error: null,
}
case GET_USER_FRIENDS_SUCCESS:
return {
...state,
isLoading: false,
loaded: true,
date: action.data,
error: null,
}
case GET_USER_FRIENDS_FAILURE:
return {
...state,
isLoading: false,
loaded: true,
date: null,
error: action.data,
}
default:
return state
}
}

Simplified new approach

// Actions and Types
const getUserProfile = actionCreator('GET_USER_PROFILE')
const getUserAddress = actionCreator('GET_USER_ADDRESS')
const getUserFriends = actionCreator('GET_USER_FRIENDS')
// Initial State
const initialAsyncState = {
isLoading: false,
loaded: false,
data: null,
error: null
}
// Initial Reducer State
const initialState = {
userProfile: initialAsyncState,
userAddress: initialAsyncState,
userFriends: initialAsyncState,
}
// Reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case getUserProfile.REQUEST:
case getUserProfile.SUCCESS:
case getUserProfile.FAILURE:
return {
...state,
userProfile: reducerHandler(state.userProfile, action, getUserProfile)
}
case getUserAddress.REQUEST:
case getUserAddress.SUCCESS:
case getUserAddress.FAILURE:
return {
...state,
userAddress: reducerHandler(state.userAddress, action, getUserAddress)
}
case getUserFriends.REQUEST:
case getUserFriends.SUCCESS:
case getUserFriends.FAILURE:
return {
...state,
userFriends: reducerHandler(state.userFriends, action, getUserFriends)
}
default:
return state
}
}

If you see, effort is almost reduced to 1/3rd with same desired effect.

Are we done? Yes! At this point, a lot of repetitive logic is reduced and code is simplified. Can we simplify this further? Maybe, but I won't advice it. We can even write a wrapper to avoid writing actions and reducers at all but it has chances of doing more harm than good.

Are there any libraries which can provide these utilities?

Yes, there are a few libraries which can do this for you but one should always think before adding extra libraries. Libraries increase the bundle size and then one has to maintain dependencies etc. That's why for simpler parts like this, writing our own logic seems preferable.

Hope, you like this article. Please like, share and comment to discuss anything which can make this approach better.


Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post