DEV Community

Discussion on: Redux command actions that scale without boilerplate

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!