Skip to content

About combineReducers #1062

Closed
Closed
@jnoleau

Description

@jnoleau

Hi, I want to share a conceptual problem I have with combineReducers in order to have the community opinion.

Example app

I have 3 actions : INCREMENT (step), DECREMENT (step), FOO;

my store is {
  count: a counter (will do something on INCREMENT, DECREMENT),
  color: will change only on INCREMENT
}

With combineReducers

I will have 2 reducers, I only show the count one below.

function count(state, action /* P3 */) {
  const step = action.step || 1;

  switch (action.type) { /* P1 */
  case 'INCREMENT':
    return state + step;

  case 'DECREMENT':
    return state - step;

  default: /* P2 */
    return state;
  }
}

P1 : the switch on action.type

This is my first problem. It sounds like an "instanceof" in OOP world very rarely used because it generally reveals that the model has an architectural problem solvable with polymorphism concept.
I think the problem is because we want here to mix functional programmation (the reducer) with Object oriented (the action payload represents an object with a type).

A solution : divide & conquer. If we split the reducer with a constraint 1 handler = 1 action

function count(state, action /* P3 */) {
  const step = action.step || 1;

  const map = {
    'INCREMENT': (state) => state + step,
    'DECREMENT': (state) => state - step
  }

  if (map[action.type] !== undefined) return map[action.type](state);

  return state;
}

Note : the partial solution is just here to understand the thought.

P2 : the "default" boilerplate

we must define the default case with a simple identity return. But why ? the lib may handle this factorization for us.

This problem is a direct consequence of the combineReducers broadcasting. Indeed combineReducers act as a broadcaster but I think a router here may be a better choice. With a router I will not have the default or the if (map[action.type] !== undefined) anymore.

The performance also is concerned but .. I know it's clearly not the breaking point of a real app so I don't hold this argument.

P3 : the signature

It's very difficult to maintain a big app without an explicit signature. Here we have an "action" but we don't know what is composed of and it is impossible because it can be all the actions (a growing set during the lifecycle of a development of an app).

"atomic" reducers

To solve all these conceptual problems I want to introduce atomic reducer : a reducer handling only one action type.

/**
 * @param {Object} action
 * @param {int} action.step 1 if undefined
 */
function countIncrementer(state, action) {
  return state + (action.step || 1);
}

Ok but ..

  1. I don't like dockblock. I mean dockblock are generally here because the name of the function is not sufficient to understand its behaviour or because the signature is imprecise. In an untyped language like Javascript we are in this case, what "action" payload means ?
  2. Divide responsibilities. My countIncrementer has to know how the count state structure is (as a count reducer), its behaviour, but the action structure .. not really, the action structure can evolve regardless of the count store.
function countIncrementer(state, step = 1) {
  return state + step;
}

Better :).

I also need a map to call the good reducer on an action. It will also have the responsibility to decrypt the action payload

// For example my partial store count.js file will export this descriptor
const count = {
  '@@redux/INIT': (state = 0) => state, // we can find a best syntax but this is the idea of default value or hydrated from createStore
  'INCREMENT': (state, action) => countIncrementer(state, action.step),
  'DECREMENT': (state, action) => countDecrementer(state, action.step)
};

And finally my main store

const store = createStore(combineAtomicReducers({
  color,
  count
}));

See complete example https://gist.github.com/jnoleau/8c30f8f4f1e70ea18c7d

Conclusion

I understand combineReducers is just an example of a rootReducer but in fact as it's included in the library I think a lot of people use it as a best practice. Maybe a solution could be to extract the combineReducers in another npm package ?

I would really appreciate your opinion about "atomic" router approach.

Thx.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions