Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

About combineReducers #1062

Closed
jnoleau opened this issue Nov 21, 2015 · 1 comment
Closed

About combineReducers #1062

jnoleau opened this issue Nov 21, 2015 · 1 comment
Labels

Comments

@jnoleau
Copy link

jnoleau commented Nov 21, 2015

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.

@gaearon
Copy link
Contributor

gaearon commented Nov 23, 2015

Thanks for the write-up! I'm closing as a duplicate of #1024, #883. Please see the discussion there.

From http://redux.js.org/docs/recipes/ReducingBoilerplate.html#generating-reducers:

Let’s write a function that lets us express reducers as an object mapping from action types to handlers. For example, if we want our todos reducers to be defined like this:

export const todos = createReducer([], {
  [ActionTypes.ADD_TODO](state, action) {
    let text = action.text.trim()
    return [ ...state, text ]
  }
})

We can write the following helper to accomplish this:

function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action)
    } else {
      return state
    }
  }
}

This wasn’t difficult, was it? Redux doesn’t provide such a helper function by default because there are many ways to write it. Maybe you want it to automatically convert plain JS objects to Immutable objects to hydrate the server state. Maybe you want to merge the returned state with the current state. There may be different approaches to a “catch all” handler. All of this depends on the conventions you choose for your team on a specific project.

The Redux reducer API is (state, action) => state, but how you create those reducers is up to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants