DEV Community

Richard Ng
Richard Ng

Posted on • Originally published at richard.ng

Redux command actions that scale without boilerplate

Should I read this post?

I think that you are more likely to find value in reading this post if you are:

  1. Trying to cut down on your Redux boilerplate; or
  2. Interested in improving your Redux architecture or file structure; or
  3. Trying to navigate Redux actions as 'commands' versus 'events'.

Key takeaways are at the foot of this post.


I recently watched a recording of a great talk by Yazan Alaboudi, 'Our Redux Anti Pattern: A guide to predictable scalability' (slides here). I really love hearing and reading about people's thoughts on Redux architecture, as something I've thought a lot about.

In the talk, Yazan makes an excellent case for two points:

  1. Writing Redux actions as commands1 is an anti-pattern; and
  2. A well-written Redux action should represent a business event.

In this particular post, I'm going to respond to the first of these points, with a view to discussing the second in a separate post.

Here, my core contention is this: Redux-Leaves solves most - and perhaps all - of Yazan's 'anti-pattern' criticisms of command actions.

I'll do this in two parts:

What is Yazan's case against command actions?

I recommend watching Yazan's own explanation, but below I will outline my interpretation of what he says.

Example code

Yazan provides some examples of command actions and their consequences:

Command Action Example (Redux setup)
  // in scoreboardReducer.js
  const INITIAL_STATE = {
    home: 0,
    away: 0
  };

  function scoreboardReducer(state = INITIAL_STATE, action) {
    switch(action.type) {
      case "INCREMENT_SCORE": {
        const scoringSide = action.payload;
        return { ...state, [scoringSide]: state[scoringSide] + 1};
      }
      default: return state;
    }
  }

  //in crowdExcitmentReducer.js
  const INITIAL_STATE = 0;

  function crowdExcitementReducer(state = INITIAL_STATE, action) {
    switch(action.type) {
      case "INCREASE_CROWD_EXCITEMENT": return state + 1;
      default: return state;
    }
  }
Command Action Consequences (Component dispatching)
  // in GameComponent
  class GameComponent extends React.Component {
    scoreGoal() {
      dispatch({ type: "INCREMENT_SCORE", scoringSide: "home"});
      dispatch({ type: "INCREASE_CROWD_EXCITEMENT"});
      // potentially more dispatches
    }

    render() {
      //...
    }
  }

In a key slide, he then lays out some costs that he sees in these command-oriented examples:

Disadvantages of Command actions

Here are my observations on each of these (with the exception of 'business semantics', which I'll tackle in a separate post):

Actions are coupled to reducers

I think that, when it comes to the example code provided by Yazan, it's extremely fair to note that actions are coupled to reducers. The "INCREMENT_SCORE" action type looks entirely coupled to the scoreboardReducer, and the "INCREASE_CROWD_EXCITEMENT" looks entirely coupled to the crowdExcitementReducer.

This is not a good pattern because it means we have extremely low code reusability. If I want to increment something else, like the stadium audience size, I need to use another action type, "INCREMENT_AUDIENCE_SIZE", even though the resulting change in state is going to be extremely similar.

Too many actions are firing

Again, when it comes to Yazan's example code, I think it's fair to note that more actions are being dispatched in the scoreGoal function that feels necessary. If a goal has been scored, a single thing has happened, and yet we're triggering multiple actions.

This is not a good pattern because it will clog up your Redux DevTools with lots of noise, and potentially could cause some unnecessary re-renders with your Redux state updating multiple times instead of doing a single large update.

Unclear why state is changing

I'm not convinced that this is a big problem. For me, in Yazan's example code, it's not too difficult for me to make the link from scoreGoal to "INCREASE_SCORE" and "INCREASE_CROWD_EXCITEMENT".

To the extent that the 'why' is unclear, I think that this can be solved by better-commented code - which isn't a situation unique to command actions in Redux, but something that applies to all imperatively-flavoured code.

Leads to a lot of boilerplate / Does not scale

I think these two are both legitimate concerns (and, at core, the same concern): if, every time we decide we want to effect a new change in state, we have to decide on a new action type and implement some new reducer logic, we will quickly get a profileration of Redux-related code, and this means that it doesn't scale very well as an approach.

How does Redux-Leaves solve these problems?

First, let's look at some example code, equivalent to the previous examples:

Redux-Leaves setup
  // store.js

  import { createStore } from 'redux'
  import reduxLeaves from 'redux-leaves'

  const initialState = {
    crowdExcitment: 0,
    scoreboard: {
      home: 0,
      away: 0
    }
  }

  const [reducer, actions] = reduxLeaves(initialState)

  const store = createStore(reducer)

  export { store, actions }
Component dispatching
  // in GameComponent
  import { bundle } from 'redux-leaves'
  import { actions } from './path/to/store'

  class GameComponent extends React.Component {
    scoreGoal() {
      // create and dispatch actions to increment both:
      //    * storeState.scoreboard.home
      //    * storeState.crowdExcitement
      dispatch(bundle([
        actions.scoreboard.home.create.increment(),
        actions.crowdExcitement.create.increment()
        // potentially more actions
      ]));
    }

    render() {
      //...
    }
  }

Here's an interactive RunKit playground with similar code for you to test and experiment with.

Hopefully, in comparison to the example of more typical command actions given by Yazan, this Redux-Leaves setup speaks for itself:

  • Only one initial state and reducer to handle
  • No more writing reducers manually yourself
  • No more manual case logic manually yourself

I'll also now cover how it addresses each of the specific problems articulated above:

Actions are no longer coupled to reducers

Redux-Leaves gives you an increment action creator out-of-the-box, that can be used at an arbitrary state path from actions.

To increment... create and dispatch this action...
storeState.crowdExcitement actions.crowdExcitement.create.increment()
storeState.scoreboard.away actions.scoreboard.away.create.increment()
storeState.scoreboard.home actions.scoreboard.home.create.increment()

You get a whole bunch of other default action creators too, which can all be effected at an arbitrary leaf of your state tree.

Too many actions? Bundle them into one

Redux-Leaves has a named bundle export, which accepts an array of actions created by Redux-Leaves, and returns a single action that can effect all those changes in a single dispatch.

  import { createStore } from 'redux'
  import reduxLeaves, { bundle } from 'redux-leaves'

  const initialState = {
    crowdExcitment: 0,
    scoreboard: {
      home: 0,
      away: 0
    }
  }

  const [reducer, actions] = reduxLeaves(initialState)

  const store = createStore(reducer)

  store.getState()
  /*
    {
      crowdExcitement: 0,
      scoreboard: {
        home: 0,
        away: 0
      }
    }
  */

  store.dispatch(bundle([
    actions.scoreboard.home.create.increment(7),
    actions.scoreboard.away.create.increment(),
    actions.crowdExcitement.create.increment(9001)
  ]))

  store.getState()
  /*
    {
      crowdExcitement: 9001,
      scoreboard: {
        home: 7,
        away: 1
      }
    }
  */

Extreme clarity on what state is changing

In Yazan's example of command actions, it's not obvious how the overall store state is going to be affected by the dispatches - which score is being incremented in which bit of state?

With Redux-Leaves, the actions API means that you are extremely explicit in which state is being changed: you use a property path to the state you want to create an action at, just as you would if you were looking at your state tree and describing which bit of state that you wanted to effect.

(This isn't addressing quite the same point Yazan makes, which I think is asking, 'but why are we increasing crowd excitement?' - but, as I indicated in discussing that point, I think it's the responsibility of the developer to make the why of a command clear through a comment, if needed.)

Incredibly minimal boilerplate

Here's what we need to do to get our root reducer and action creators:

import reduxLeaves from 'redux-leaves'

const [reducer, actions] = reduxLeaves(initialState)

That's it. Two lines, one of which is an import. No faffing around with writing action constants, creators or reducer case statements yourself.

Simple to scale

Suppose we want to introduce some state to keep track of team names.

All we need to do is to change our initial state...

import reduxLeaves from 'redux-leaves'

const initialState = {
  crowdExcitement: 0,
  scoreboard: {
    home: 0,
    away: 0
  },
+  teams: {
+    home: 'Man Red',
+    away: 'Man Blue'
  }
}

const [reducer, actions] = reduxLeaves(initialState)
const store = createStore(reducer)

... and then we can straight away start dispatching actions to update that state, without having to write any further reducers, action creators or action types:

store.dispatch(actions.teams.away.create.update('London Blue'))
store.getState().teams.away // => 'London Blue'

Takeaways


Endnotes

1 An Redux action can be considered a command if it expresses intent to do something. Examples given by Yazan are:

{ type: 'SEND_CONFIRMATION' }
{ type: 'START_BILLING' }
{ type: 'SEND_LETTER' }
{ type: 'INCREMENT_SCORE' }
{ type: 'INCREASE_CROWD_EXCITEMENT' }

MAIN

Top comments (2)

Collapse
 
alaboudi profile image
Yazan Alaboudi

I am really happy that this is becoming more of a discussion point. But with that, I think redux-leaves streamlines a problem and doesn't necessarily address it.

Few points to make here. I don't think it's fair to say the scoreboard and crowd should be captured in the same reducer. The main reason I say this is because there may be other reasons to why the crowd excitement may change (The kiss cam can go on, free popcorn, cheerleaders, etc) . If you put them in the same reducer you are falling victim to breaking the Cohesion Principle in software development.

Another point to make, by dispatching all the actions in the component (even if they are bundled), you would still be coupling your components to those reducers. If there is a point where you'd have another reducer that is interested in understanding when a goal is scored, you'd have to edit the component to include that action (edit the bundle). That itself is a sign of coupling.

Also, it is very beneficial not to know where your actions are going because the relationship between an action and reducer should be many to many:

  1. the event of scoring a goal effects the scoreboard and the crowd simultaneously (1 action to many reducers)

  2. the crowd gets excited when goal is scored, when popcorn is being served, when the kiss cam is on. ( many actions to 1 reducer).

Collapse
 
richardcrng profile image
Richard Ng • Edited

Thanks for taking the time to read and respond - and I'm also really glad that we're having this discussion! Redux-Leaves came out of my experience of battling with a sprawling mess of files and reducers and how tedious it all felt. However, I was managing Redux in very much the command action way, and not the eventful action way, and so the library came together mostly as a means of streamlining problems there.

I'm now really interested in seeing whether the library can be made to work for those who would prefer eventful actions, so I really value your feedback on this. There are two questions I have there:

  1. Is the existing Redux-Leaves API sufficient to address concerns of those who like eventful actions; and
  2. If not, can I add things to Redux-Leaves to address any outstanding concerns?

My first plan is to explore whether the existing API is sufficient (obviously), so I'm going to try to make the case that it is sufficient - not because I'm trying to 'win', but because I really want to battle-test the API. I hope that, if I am unsuccessful in defending it as it is, I'll learn what I need to do to make it work for your use cases - so thank you for your honesty and candour.

In my reading of your comment, you're articulating two points of contention with the Redux-Leaves I've given above: (1) by the Cohesion Principle, scoreboard and crowdExcited state shouldn't be in the same reducer; and (2) components should not be coupled to reducers.

(I hope my reading is correct, as that's what I'm going to try to address.)

I'm going to respond to those in reverse order, my contentions being:

  1. You can encapsulate Redux-Leaves actions in a way that mean components are no more meaningfully coupled to the root Redux-Leaves reducer shape than would be true through an eventful action pattern that doesn't use Redux-Leaves; and
  2. Since all store state ends up in the same root reducer anyway, it is a good trade-off to use Redux-Leaves for boilerplate elimination at the cost of not being able to control scoreboardReducer directly.

Component should not be coupled to reducers

I think this can be solved through encapsulation:

// actions.js
export const goalScoredBy = (side = 'home') => {
  const didHomeScore = side === 'home'
  return bundle([
    actions.crowdExcited.create.increment(didHomeScore ? 100 : -100),
    actions.scoreboard[side].create.increment(1)
  ])
}

// GameComponent.js
import { goalScoredBy } from './redux/actions'

class GameComponent extends React.Component {
  scoreGoal() {
    dispatch(goalScoredBy('home'));
  }

  render() {
    //...
  }
}

Given the above setup:

If there is a point where you'd have another reducer that is interested in understanding when a goal is scored

now we just add one line to actions.js, e.g.

export const goalScoredBy = (side = 'home') => {
  const didHomeScore = side === 'home'
  return bundle([
    actions.crowdExcited.create.increment(didHomeScore ? 100 : -100),
    actions.scoreboard[side].create.increment(1),
+   actions.commentary.create.push('GOOOOAAAAALLL!!!!')
  ])
}

which, in my opinion: (a) does not require any more changes to the GoalComponent.js file than under the eventful action pattern; and (b) is less boilerplate than adding an additional case to commentaryReducer.

Perhaps this encapsulation step is a way of implementing eventful actions through Redux-Leaves? I'd be interested in your thoughts on that.


scoreboard and crowdExcited state should not be in the same reducer

As you'll know, with Redux, all your state is in the same root reducer - that might just be one created by repeated calls to combineReducers over some child reducers.

As such, the way I'm interpreting your point is: even though scoreboard and crowdExcited will ultimately end up in the same reducer, there is still a net benefit to creating child reducers for the two.

The extent to which I agree with that depends on whether Redux-Leaves is used or not.

Without Redux-Leaves

I think that is very plausible to say that there is a net benefit to creating those child reducers in a situation when the alternative is writing and manually maintaining a single reducer that holds both parts of state.

Benefits of child reducers
  • reducer logic is simpler and cleaner;
  • reducers are easier to test; and
  • all scoreboard related changes are grouped together.
Costs of child reducers
  • more files to maintain; and
  • your overall state tree's structure becomes a little more opaque (imo).

To elaborate on the second: I think it makes it harder to get that overall bird's-eye view of your state tree as you're writing your code, because it's not all in one place, but split up into little chunks. This might not be a big cost, because you might very infrequently want the bird's-eye view of state, but I think there are occasions when I do want it (e.g. when writing selector functions), and so I think it's a non-zero cost.

But, to reiterate, I think it is very plausible that, in spite of the costs, there is a net benefit to creating child reducers to manage scoreboard and crowdExcited state respectively in a situation when the alternative is writing and maintaining a single reducer.

With Redux-Leaves

My claim is that Redux-Leaves eliminates the first two benefits of creating child reducers, as articulated above:

  • the simplest and cleanest code is const [reducer, actions] = reduxLeaves(initialTreeState); and
  • you don't need to test the resulting reducer since the Redux-Leaves library has documentation and tests.

I accept that there is a benefit that is then lost, in a sense - now, all scoreboard related changes are not grouped together.

I contend that this is an acceptable trade-off considering that, with Redux-Leaves, you have:

  • far fewer files to maintain (it's literally just two lines of setup); and
  • a really clear initialTreeState which responds really predictably to dispatched actions.

Summary

So, in summary, I claim that:

  1. You can encapsulate Redux-Leaves actions in a way that mean components are no more meaningfully coupled to the root Redux-Leaves reducer shape than would be true through an eventful action pattern that doesn't use Redux-Leaves; and
  2. Since all store state ends up in the same root reducer anyway, it is a good trade-off to use Redux-Leaves for boilerplate elimination at the cost of not being able to control scoreboardReducer directly.

If you think that I am wrong, I'd love to know where, as this will all help me to improve the library!