Refactoring a Redux application using redux-actions
Redux is widely used and a great way to manage your data flow in a React app, but a common complaint is that Redux requires a lot of boilerplate code. I was looking for a way to reduce the boilerplate while keeping code expressiveness. In the official Redux documentation, they mention different solutions, but my attention got drawn by redux-actions. It follows Flux Standard Action convention and has a small API that covers the creation and handling of actions.
In this example, we have a duck module that manages to fetch and store articles. We use redux-thunk for handling asynchronous actions.
import * as api from '../api/articles';
// Types
export const BEGIN_REQUEST = 'ARTICLES/BEGIN_REQUEST';
export const GET_ARTICLES = 'ARTICLES/GET_ARTICLES';
export const GET_ARTICLES_COMPLETE = 'ARTICLES/GET_ARTICLES_COMPLETE';
export const status = {
LOADING: 'LOADING',
ERROR: 'ERROR',
COMPLETE: 'COMPLETE',
};
// Actions
export const beginRequest = () => ({
type: BEGIN_REQUEST,
});
export const getArticlesComplete = (result) => {
if (result instanceof Error) {
return {
type: GET_ARTICLES_COMPLETE,
payload: result,
error: true,
};
}
return {
type: GET_ARTICLES_COMPLETE,
payload: result,
};
};
export const getArticles = () => (dispatch) => {
dispatch(beginRequest());
return api.getArticles().then(
(entities) => {
dispatch(getArticlesComplete(entities));
},
(error) => {
dispatch(getArticlesComplete(error));
}
);
};
// Reducer
const initialState = {
status: null,
error: null,
entities: null,
};
export default (state = initialState, { type, payload, error }) => {
switch (type) {
case BEGIN_REQUEST:
return {
...state,
status: status.LOADING,
};
case GET_ARTICLES_COMPLETE:
if (error) {
return {
...state,
status: status.COMPLETE,
error: payload,
};
}
return {
...state,
status: status.COMPLETE,
entities: [...state.entities, ...payload],
error: null,
};
default:
return state;
}
};
This example is a “classic” way of defining action creators and a reducer. Where I think we can reduce boilerplate is when we need to handle different payloads’ states, depending on if it is an error or the entities. Every time we need to create a “complete” action creator, like getArticlesComplete
, we have to check the type of the payload to know if it is a failure or a success. This code is redundant and doesn’t bring valuable information, and thereby it can be extracted into a helper function.
Refactoring using redux-actions
- Actions
import { createAction } from 'redux-actions';
// ...
export const beginRequest = createAction(BEGIN_REQUEST);
export const getArticlesComplete = createAction(GET_ARTICLES_COMPLETE);
// ...
redux-actions has a helper createAction
that does the work for us to handle the different payloads’ states. Moreover, by using this helper, we know that these actions can have success and failure states.
Note: We haven’t yet refactored getArticles
since it is an async action and has a different logic.
- Reducer
import { handleActions } from 'redux-actions';
// ...
export default handleActions(
{
[BEGIN_REQUEST]: (state) => ({
...state,
status: status.LOADING,
}),
[GET_ARTICLES_COMPLETE]: {
next: (state, { payload }) => ({
...state,
status: status.COMPLETE,
entities: [...state.entities, ...payload],
error: null,
}),
throw: (state, { payload }) => ({
...state,
status: status.COMPLETE,
error: payload,
}),
},
},
initialState
);
handleActions
let us define our reducer. The big change is that we handle the success and failure states using next
(success) and throw
(failure) instead of using the if
statement. The positive part is that now each reducer is defined into separated functions, so it prevents data leaking between action types. Other than that, the handleActions
solution is close to the switch statement in terms of readability.
Moving forward using reduce-reducers
import { createAction, handleAction } from 'redux-actions';
import reduceReducers from 'reduce-reducers';
// ...
const initialState = {
status: null,
error: null,
entities: null,
};
// BEGIN_REQUEST
export const beginRequest = createAction(BEGIN_REQUEST);
const beginRequestReducer = handleAction(
BEGIN_REQUEST,
(state) => ({
...state,
status: status.LOADING,
}),
initialState
);
// GET_ARTICLES
export const getArticlesComplete = createAction(GET_ARTICLES_COMPLETE);
export const getArticles = () => (dispatch) => {
dispatch(beginRequest());
return api.getArticles().then(
(entities) => {
dispatch(getArticlesComplete(entities));
},
(error) => {
dispatch(getArticlesComplete(error));
}
);
};
const getArticlesReducer = handleAction(
GET_ARTICLES_COMPLETE,
{
next: (state, { payload }) => ({
...state,
status: status.COMPLETE,
entities: [...state.entities, ...payload],
error: null,
}),
throw: (state, { payload }) => ({
...state,
status: status.COMPLETE,
error: payload,
}),
},
initialState
);
export default reduceReducers(beginRequestReducer, getArticlesReducer);
reduce-reducers allows us to reduce multiple reducers into a single reducer. So, what it means is that we can define each reducer (BEGIN_REQUEST
, and GET_ARTICLES_COMPLETE
) into their own handleAction
. What I like about this solution is that we can gather related code from the action type to the action creator and the reducer and put it in the same part of the ducks file.