Killing bad parts of Redux. Say goodbye to boilerplate.

Redux is the most popular state management library in JavaScript ecosystem for Single Page Applications. However, probably it would be much more popular if not infamous statements, like Redux is verbose, Redux boilerplate and so on. In my opinion though, there is only one part of Redux which could be easier to use, namely Redux actions. In this article I will try to point some issues with Redux actions and what we could do to mitigate them.

Not necessarily verbose parts in Redux

Before we begin, let’s talk about two things which could be considered as verbose, but in my view are not.

Separate actions and reducers

There are many complains that in Redux you need to write actions and reducers separately. For me this is a good thing and actually this was done by design. We shouldn’t think that actions and reducers have 1 to 1 relationship. One reducer can react to many separate actions… and many reducers can react to the very same action. This is one of the most powerful features of Redux, often not appreciated.

Switch statements in reducers

Many of us hate switch statements in reducers. This is opinionated though and there are many libraries which allow to write reducers in different ways. We will write such a helper a little later in this article too!

Truly verbose parts in Redux

For me, the most problematic parts of Redux are related to actions, constants and thunks. What’s more, those problems are not only about verbosity, but also about potential bugs, like types collision. Let’s name those issues and try to fix them one by one, until there is nothing left!

Constants

In my head, this was always the most annoying thing in Redux. Writing separate actions and constants is not only verbose, but also error-prone. Moreover, it also introduces some disorder to our imports. For example, you need constants to recognize actions, but you need actions (action creators to be precise, but let me stick with actions shortcut for simplicity) to be able to dispatch them. Often you end up importing an action and a constant related to the very same action! What if we could give up constants altogether without any compromise? Let’s try to write a helper function!

const createAction = (name, action = () => ({})) => {
  const actionCreator = (...params) => ({
    type: name,
    ...action(...params),
  });
  actionCreator.toString = () => name;
  return actionCreator;
};

So, what we just did? Instead of explaining, let’s just try to use it! Imagine we have an action like that:

const INCREMENT_BY_VALUE = 'INCREMENT_BY_VALUE';

const incrementByValue = value => ({
  type: INCREMENT_BY_VALUE,
  value,
)};

We could rewrite it like that now:

const incrementByValue = createAction(
  'INCREMENT_BY_VALUE',
  value => ({ value }),
);

As you can see, we pass INCREMENT_BY_VALUE type as the 1st argument to createAction, which does the rest job for us. But wait a second, we don’t have constants anymore, so how we could use it in reducers for example? The key is actionCreator.toString = () => name line in createAction body, which allows us to get action type constant like incrementByValue.toString(). So, the action is the source of its type at the same time, so no more keeping constants and actions in sync, you need just actions and you are done! As a bonus, sometimes you won’t even need to call toString() manually, see how in the next paragraph!

Avoiding manual toString calls in reducers

Before we solve this issue, see how a reducer reacting to incrementByValue action could look like:

const valueReducer = (state = 0, action) => {
  switch (action.type) {
    case incrementByValue.toString():
      return state + action.value;
    default:
      return state;
  }
};

It uses the standard switch statement, which some people love and some people hate, the only issue in comparison to normal reducers is this nasty incrementByValue.toString(), which is needed to get the proper INCREMENT_BY_VALUE type. Fortunately for switch and toString haters, there is a solution, let’s create a reducer helper function:

const createReducer = (handlers, defaultState) => {
  return (state, action) => {
    if (state === undefined) {
      return defaultState;
    }

    const handler = handlers[action.type];

    if (handler) {
      return handler(state, action);
    }

    return state;
  };
};

Now, we could refactor valueReducer as:

const valueReducer = createReducer({
  [incrementByValue]: (state, action) => state + action.value,
}, 0);

As you can see, no switch or toString anymore! Because we replaced switch with handlers object, we can use computed property [incrementByValue], which calls toString automatically!

Thunks

For many developers thunks are used to create side-effects, often as an alternative to redux-saga library. For me they are something more though. Often I need an argument in my actions, but such an argument which is already present in Redux store. Again, there are many opinions about this, but for me passing to action something already present in the store is an antipattern. Why? Imagine you use Redux with React and you dispatch an action from React. Imagine that this action needs to be passed something already kept in the store. What would you do? You would read this value by useSelector, connect or something similar first, just to pass it to the action. Often this component wouldn’t even need to do that, because this value could be only action’s dependency, not React component’s directly! If Redux action could read the state directly, this React component could be much simpler! So… thunks to the rescue! Let’s write one!

const incrementStoredValueByOne = () => (dispatch, getState) => {
  const { value } = getState(); // we could use selector here
  return dispatch({
    type: 'INCREMENT_STORED_VALUE_BY_ONE',
    newValue: value + 1,
  });
};

Before we continue, of course this example might to too naive, we could solve this problem by a proper logic in reducer, it is just to illustrate the problem. Anyway, notice, that this thunk reads current value from the store instead of getting it as an argument. Problem solved then! Not so quick! Again, what about types? If you need to refactor an action to thunk just to read state from Redux directly, you will end up with the constants issue we already solved by createAction again. So what should we do? Do something similar but just for thunks!

const createThunk = (name, thunk) => {
  const thunkCreator = (...params) => (dispatch, getState) => {
    const actionToDispatch = thunk(...params)(dispatch, getState);
    return dispatch({ type: name, ...actionToDispatch });
  };

  thunkCreator.toString = () => name;
  return thunkCreator;
};

Now, we could refactor our thunk like that:

const incrementStoredValueByOne = createThunk(
  'INCREMENT_STORED_VALUE_BY_ONE',
  () => (dispatch, getState) => {
    const { value } = getState(); // we could use selector here
    return { newValue: value + 1 };
  },
};

Again, no constants! incrementStoredValueByOne.toString() will return INCREMENT_STORED_VALUE_BY_ONE, so you could even listen to this thunk in your reducers directly!

Other problems

We solved many issues already, but unfortunately there are more:

  1. You still need to pass action type in createAction or createThunk as the first argument, which is kind of duplication. It would be cool if we could define actions like const myAction = createAction() instead of const myAction = createAction('MY_ACTION')
  2. What about the risk of action types collision? What if 2 of your actions will have the very same name? The bigger the application, the bigger chance this could happen. There are already libraries, which try to fix that, for example by adding a counter to types. However, those solutions are not deterministic, which will cause troubles with Hot Module Replacement and possibly Server Side Rendering.
  3. createAction and createThunk should have some Typescipt types, otherwise you won’t get proper autocomplete in a text editor like Visual Studio Code.
  4. Should we really care about those things during writing applications? We should have a ready to use solution!

Fortunately, now such a solution exists…

Introducing redux-smart-actions library

Let me introduce redux-smart-actions library, the fastest way to write Redux actions!

This library provides all the utilities like createAction, createThunk, createReducer, and at the same time solves all mentioned issues not covered in this article. Points 1 and 2 are solved by the optional babel-plugin-redux-smart-actions. Point 3 is solved as Typescript types are included in the library. And point 4… is solved by any library anyway, including this one ;)

Basically with its help you could transform your code like that:

+ import {
+   createSmartAction,
+   createSmartThunk,
+   createReducer,
+   joinTypes,
+ } from 'redux-smart-actions';
+
- const RESET_VALUE = 'RESET_VALUE';
- const SET_VALUE = 'SET_VALUE';
- const INCREMENT_IF_POSITIVE = 'INCREMENT_IF_POSITIVE';
-
- const resetValue = () => ({ type: RESET_VALUE });
+ const resetValue = createSmartAction();

- const setValue = value => ({ type: SET_VALUE, value });
+ const setValue = createSmartAction(value => ({ value }));

- const incrementIfPositive = () => (dispatch, getState) => {
+ const incrementIfPositive = createSmartThunk(() => (dispatch, getState) => {
    const currentValue = getState().value;

    if (currentValue <= 0) {
      return null;
    }

-   return dispatch({
-     type: INCREMENT_IF_POSITIVE,
-     value: currentValue + 1,
-   });
+   return { value: currentValue + 1 });
- };
+ });

- const valueReducer = (state = 0, action) => {
-   switch (action.type) {
-     case RESET_VALUE:
-       return 0;
-     case SET_VALUE:
-     case INCREMENT_IF_POSITIVE:
-       return action.value;
-     default:
-       return state;
-   }
- }
+ const valueReducer = createReducer({
+   [resetValue]: () => 0,
+   [joinTypes(setValue, incrementIfPositive)]: (state, action) => action.value;
+ }, 0);

Don’t be afraid that this library is new, I use it in several very big projects already without any issues, so I very recommend you to at least try it! If you happen to like it, any token of appreciation like giving a star to the github repo is very much welcome!