DEV Community

Cover image for Rethinking Redux (and reducing its boilerplate)
Emilio Srougo
Emilio Srougo

Posted on

Rethinking Redux (and reducing its boilerplate)

tl;dr: redux-create-module

Suppose you want to create a new module in your react-redux app.
You know the drill.
Create the action type constants, make and the action creators, and handle the types in the reducer.
You may have noticed that the process looks the same almost every time.
We, as developers, know that this kind of things could, and should be abstracted and automated.

So, first let's think what's the best solution for Developer Experience.
Or, since we are the Developers, let's just Rethink Redux.

Well, if we are only interested in mapping actions to states, my first thought is to make a Map. Or a regular object, for that matter.

const counterMap = {
  increment: (state) => state + 1,
  decrement: (state) => state -1
}
// How do we use it? Simple. With a reducer.
const reducer = (state = 0, action) => 
  (counterMap[action.type]) 
    ? counterMap[action.type](state) 
    : state
Enter fullscreen mode Exit fullscreen mode

But we don't want to create the reducer. We only want to think about the Map. Let's make a function that takes a Map and returns a reducer

const createReducer = (initialState, map) =>
   (state = initialState, action) => 
     (map[action.type]) 
       ? map[action.type](state) 
       : state

const reducer = createReducer(0, counterMap)

reducer(0, {type: "increment"}) // => 1
Enter fullscreen mode Exit fullscreen mode

So simple! Or, too simple! Because we have a problem here. Action creators were invented for a reason, and that's because some actions need to have a payload to be handled... Or is it?
No! of course, there is another reason and that's because it's unsafe and ineffective to pass strings to the reducer. What if we make a typo on it?!
So let's get more serious now.

We still don't want to create the action creators manually, and, why should we?
Think about it, createReducer already has all the information it needs to make them.
It can get the types of our actions from the keys of the Map.
So, let's make it return both, a reducer and the action creators, and name it createModule

const createModule = (initialState, map) => {
  const reducer = (state = initialState, action) => 
     (map[action.type]) 
       ? map[action.type](state) 
       : state
  const actions = Object.keys(map).reduce(
    (acc, type) =>
      Object.assign(acc, {
        [type]: (payload = {}) => ({ type, payload })
      }),
    {}
  );
}  
// example
const counterMap = {
  add: (state, {payload}) => state + payload
}
export const counter = createModule(0, counterMap)

// then somewhere in a connected component...
dispatch(counter.actions.add(2))
Enter fullscreen mode Exit fullscreen mode

Neat!

Of course, we still have some todos. like namespacing our actions to avoid conflicts, and allowing a reducer to handle an action from another module.
But we won't get into that in this post.

Instead, I'll refer you to the source of the little module I made.

Thanks for reading!

Photo: Sunrise in the Sea by me :)

Top comments (0)