Skip to content

Instantly share code, notes, and snippets.

@deanrad
Last active December 27, 2020 18:31
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save deanrad/f380994d117dffb5b625a7871a4fa893 to your computer and use it in GitHub Desktop.
Save deanrad/f380994d117dffb5b625a7871a4fa893 to your computer and use it in GitHub Desktop.
TL;DR Better Redux involves using maps of action types to reducers, not switch/case statements

Distilling the Essence of Reducers

Redux has brought the notion of reducer back into the awareness of many developers for whom they are a novel concept. In fact they are quite simple, and used all the time in such things as SUM aggregations in databases, where they compute a single value from many.

It's great that Redux has made reducers known to a broader audience, though they are relatively ancient concepts in programming, in fact. But the particular way Redux illustrates a reducer in its documentaion is, in my opinion, with a coding style that is harder to extend and read than it should be. Let's distill reducers down to their essensce, and build up Redux reducers in a way that lowers complexity, and helps separate Redux idioms from your business logic.

The simplest reducer

A reducer is a pure function that accepts more arguments than it returns. That is to say - one whose "arity" is greater than 1. It 'reduces' the two things you pass it down to a single value. Here are two reducers, in a map:

let reducers = {
  ADD: (a, b) => a + b,
  MULT: (a, b) => a * b
}

Either of these can be used by Array.reduce as follows:

let result = [1, 2, 3].reduce(reducers.ADD)
// 6

The normal behavior of reducers is to create the initial value by passing the first two values to the reducer. In the above example, this would mean the reducer was only invoked twice. But if we wanted to start from an initial value:

let result = [1, 2, 3].reduce(reducers.ADD, 4)
// 10

Then we'd have the more Redux-like behavior of invoking the reducer once per item being reduced.

In Redux, the reduction metaphor fits, because you have a series of actions, and an initial state, and you are always reducing the two values state and action down into a new state value. But the actions are not plain Numbers like the example above.

Redux recognizes that in any complex enough application there is more than one action a user can take, so it assumes that each action is an object with a field called type. Furthermore it assumes that the reducer will invoke different code based on that type field. All this is good, i just think Redux simply goes wrong in how it suggests one implements this, when it provides examples like these:

let initialState = 1.5
let actions = [{type: 'MULT', value: 2}, {type: 'ADD', value: 2}]
let reducer = (state=initialState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.value
    case 'MULT':
      return state * action.value
    default:
      return state
  }
}

Lets see how much simpler the code could be if we instead made use of the reducers we showed earlier in the example:

let reducers = {
  ADD: (a, b) => a + b,
  MULT: (a, b) => a * b
}

let leaveStateUnchanged = (state) => state

let reducer = (state, action) => {
  if (state === null) return initialState
  let reducer = reducers[action.type] || leaveStateUnchanged
  return reducer(state, action.payload)
}

That is a heck of a lot better.

It's better in 1) how easily the reducer's behavior is extended to new action type/reducer mappings, 2) how the code is factored into Redux and non-Redux parts, and 3) in how the Redux idioms are isolated to their own lines.

This function explains clearly on its first line that if it is passed a null value for state, it provides an initial value.

Secondly, since Redux expects that we leave the state unchanged if we don't recognize the action's type, we codify this with an identity function. This lets our intent scream out clearly:

If no reducer corresponds to this action type, use the identity function to return the state unchanged

But the big win is in our reducers. These reducers are pure functions which are not Redux ™ reducers which expect objects of a specific shape. They are allowed to focus on just what they do, and if you want to name their arguments state and action you're welcome to, but that's not even required; they are framework agnostic. And you add more of them simply by adding entries to a map, not by modifying the reducer. Less chance for syntactic error, better adherence to the Open-Closed Principle. Just better.

So how can you use this today? You can write your reducer in the style I showed above without any 3rd party library. Or if you want to get a few other pieces of functionality as well as have a prewritten function to create a reducer from a Map, you can use the fine redux-act library and write the following:

createReducer({
  ADD: (a, b) => a + b,
  MULT: (a, b) => a * b
}, 1.5)

In conclusion, it's fine that Redux introduces the functional notion of reducers to a wide audience. It's just unfortunate that concepts like looking up first-class reducer functions in a map did not make the Redux docs, and the example of writing ever-growing conditionals was put forth as a good practice. Fortunately, with the many eyes on this library that we all have, we can harvest all the good ideas going forward.

@EnoahNetzach
Copy link

Woah!!

@radubrehar
Copy link

We're doing redux in the same way with https://www.npmjs.com/package/redux-map-reducers

@austinhyde
Copy link

There's also the redux-actions library, which has lot more traction than redux-act or redux-map-reducers.

@coryhouse
Copy link

initialState is undefined in the refactored example. I suggest adding that for clarity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment