Removing Boilerplate with Redux-Actions

John Tucker
Jul 25, 2017 · 5 min read

Using Redux-Actions library provides a simple incremental improvement to your Redux implementation. The improvements comes in the form of replacing imperative (describes how to do it) implementations with a more declarative (describe what the outcome is to be) ones.

This article assumes that you are fairly familiar with the Redux library; if not, I wrote a series of articles on the topic starting with:

In this article I refactor a representative Redux implementation using Redux-Actions.

Flux Standard Action

First, we need to make a quick detour and talk about the Flux Standard Action specification.

It’s much easier to work with Flux actions if we can make certain assumptions about their shape…. Defining a minimal, common standard for these patterns enables the creation of useful tools and abstractions.

— Flux Standard Action Team

Agreeing upon a standard shape of Redux (a Flux implementation) actions is one of the keys to reducing boilerplate code. At the simplest level, this means we need to construct actions with the following sample structure; observe the type and payload properties.

{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}

Starting Point (Reselect)

We first by reviewing the starting point of the example refactor.

To build:

  1. Install Node.js.
  2. Download and extract the repository Redux Patterns (also used in the Redux By Example series).
  3. From the extracted folder, run npm install
  4. Run the command ./node_modules/.bin/babel src -d dist

Looking at the starting point source code:

  • The state consists of two leaves, byId and ids; used to store an array of objects (items) with primitive properties.
  • The implementation has four actions, FETCH, UPDATE, ADD, and REMOVE.
  • There are only two action creators, fetch and update (for simplicity, I did not implement the add and update ones that are virtually identical to update).

Thinking about this implementation, one error-prone issue is the repetition of the strings for the action’s type property, e.g., FETCH, across both the reducers and action creators.

The use of the switch statement is an example of a repetitive imperative implementation in the Redux reducers; more error-prone code.

Redux-Actions

Now building the refactored example.

First, I installed Redux-Actions from the command line in the download root folder with:

npm install --save redux-actions

By commenting out (and a little re-arranging) code we end up with the updated source code with the following changes:

We first bring in the necessary dependencies from redux-actions.

...
import { createSelector } from 'reselect';
import { combineActions, createActions, handleActions } from 'redux-actions';
...

Because, using redux-actions begins with defining the action creators, we move the action creators section to the top and replace (adding in the add and remove actions) them with the following:

...
// ACTION CREATORS
/*
const fetch = items => ({
type: 'FETCH',
value: normalize(items, itemsSchema),
});
const update = item => ({
type: 'UPDATE',
value: normalize(item, itemSchema),
});
*/
const actionCreators = createActions({
FETCH: items => normalize(items, itemsSchema),
UPDATE: item => normalize(item, itemSchema),
ADD: item => normalize(item, itemSchema),
REMOVE: item => normalize(item, itemSchema),
});

...

The result is that actionCreators has the properties, fetch, update, add, and remove (generated as lower-case from the action type strings) that are the action creators, i.e., they are functions that return action objects.

It is important to note that this implementation has the bare minimum amount of information to construct the action creators, i.e., no extra boilerplate code.

We now replace the reducers byId and ids as follows; starting with the simpler ids:

...
/*
const ids = (state = [], action) => {
switch (action.type) {
case 'FETCH':
return action.value.result;
case 'ADD':
return [...state, action.value.result];
case 'REMOVE': {
const newState = [...state];
newState.splice(state.indexOf(action.value.result), 1);
return newState;
}
default:
return state;
}
};
*/
const ids = handleActions({
[actionCreators.fetch](state, action) {
return action.payload.result;
},
[actionCreators.add](state, action) {
return [...state, action.payload.result];
},
[actionCreators.remove](state, action) {
const newState = [...state];
newState.splice(state.indexOf(action.payload.result), 1);
return newState;
},
}, []);
...

Like the action creators, this implementation has the bare minimum amount of information to construct the reducer. This code might have some unfamiliar syntax (it was for me). First, you need to familiar with shorthand method names:

const o = {
fetch(state, action) { console.log(action); }
};

The result of this is an object with a method, fetch, that accepts two parameters state and action, and with an implementation that logs action to the console.

Next you need to be familiar with computed property names:

const prop = 'fetch';
const o = {
[prop](state, action) { console.log(action); }
};

This has the exact result as the previous example; but the property name is computed at run-time.

Finally, you need to understand that if the value inside the square brackets is not a string, it is converted to one using the toString object method and that the action creators created by createActions have a toString the returns action string, i.e., actionCreators.fetch.toString() is the string FETCH.

The result of this fancy syntax is that the first parameter of handleActions is an object with properties consisting of the action strings with values being the function that handles the respective case (much like the switch / case statements). The second parameter is the initial state.

The byId reducer implementation is similar except that we use combineActions to provide the same function for each of the properties: FETCH, UPDATE, and ADD.

// REDUCERS
/*
const byId = (state = {}, action) => {
switch (action.type) {
case 'FETCH':
case 'ADD':
case 'UPDATE': {
return {
...state,
...action.value.entities.items,
};
}
case 'REMOVE': {
const newState = { ...state };
delete newState[action.value.result];
return newState;
}
default:
return state;
}
};
*/
const byId = handleActions({
[combineActions(
actionCreators.fetch,
actionCreators.update,
actionCreators.add,
)](state, action) {
return { ...state, ...action.payload.entities.items };
},
[actionCreators.remove](state,action) {
const newState = { ...state };
delete newState[action.payload.result];
return newState;
},
}, {});

note: If you are wondering what is happening under the hood of combineActions, it returns the string FETCH||UPDATE||ADD which is interpreted by handleActions as a special case.

Outside of the changes to the action creators and reducers, the rest of the code is unchanged. Execute the following from the extracted folder to run (same result as we had before we refactored the code):

node ./dist/redux-actions.js

Conclusion

While using redux-actions does not dramatically shrink the code, it does strip down the action creators’ and reducers’ code to a bare minimum with no duplicated strings or imperative code.

John Tucker

Written by

Senior Consultant @ Appirio

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at http://frontendweekly.co

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade