44

I don't understand what reduce-reducers is meant for. Should it be used in the case that I have 2 reducer functions containing the same action?

function reducerA(state, action){
   switch(action.type):
       ...
       case 'SAME_ACTION': {...state, field: state.field+1}
}

function reducerB(state, action){
   switch(action.type):
       ...
       case 'SAME_ACTION': {...state, field: state.field*2}
}

So if I call reduceReducer on reducerA and reducerB and action 'SAME_ACTION' is invoked for {field: 0} then I would have a next state {field: 2}?

Also it seems to me that it kind of concatenates reducers (meaning merging them under one key).

Am I right or does reduceReducer serve a different purpose?

see sharper
  • 8,591
  • 5
  • 29
  • 51
Amio.io
  • 17,083
  • 11
  • 66
  • 100

2 Answers2

106

The difference is:

  • combineReducers creates nested state
  • reduceReducers creates flat state

Consider following reducers. There are no action types to make things simpler:

// this reducer adds a payload to state.sum 
// and tracks total number of operations
function reducerAdd(state, payload) {
  if (!state) state = { sum: 0, totalOperations: 0 }
  if (!payload) return state

  return {
    ...state,
    sum: state.sum + payload,
    totalOperations: state.totalOperations + 1
  }
}

// this reducer multiplies state.product by payload
// and tracks total number of operations
function reducerMult(state, payload) {
  if (!state) state = { product: 1, totalOperations: 0 }
  if (!payload) return state

  // `product` might be undefined because of 
  // small caveat in `reduceReducers`, see below
  const prev = state.product || 1

  return {
    ...state,
    product: prev * payload,
    totalOperations: state.totalOperations + 1
  }
}

combineReducers

Each reducer gets an independent piece of state (see also http://redux.js.org/docs/api/combineReducers.html):

const rootReducer = combineReducers({
  add: reducerAdd,
  mult: reducerMult
})

const initialState = rootReducer(undefined)
/*
 * {
 *   add:  { sum: 0, totalOperations: 0 },
 *   mult: { product: 1, totalOperations: 0 },
 * }
 */


const first = rootReducer(initialState, 4)
/*
 * {
 *   add:  { sum: 4, totalOperations: 1 },
 *   mult: { product: 4, totalOperations: 1 },
 * }
 */    
// This isn't interesting, let's look at second call...

const second = rootReducer(first, 4)
/*
 * {
 *   add:  { sum: 8, totalOperations: 2 },
 *   mult: { product: 16, totalOperations: 2 },
 * }
 */
// Now it's obvious, that both reducers get their own 
// piece of state to work with

reduceReducers

All reducers share the same state

const addAndMult = reduceReducers(reducerAdd, reducerMult) 

const initial = addAndMult(undefined)
/* 
 * {
 *   sum: 0,
 *   totalOperations: 0
 * }
 *
 * First, reducerAdd is called, which gives us initial state { sum: 0 }
 * Second, reducerMult is called, which doesn't have payload, so it 
 * just returns state unchanged. 
 * That's why there isn't any `product` prop.
 */ 

const next = addAndMult(initial, 4)
/* 
 * {
 *   sum: 4,
 *   product: 4,
 *   totalOperations: 2
 * }
 *
 * First, reducerAdd is called, which changes `sum` = 0 + 4 = 4
 * Second, reducerMult is called, which changes `product` = 1 * 4 = 4
 * Both reducers modify `totalOperations`
 */


const final = addAndMult(next, 4)
/* 
 * {
 *   sum: 8,
 *   product: 16,
 *   totalOperations: 4
 * }
 */

Use cases

  • combineReducers - each reducer manage own slice of state (e.g. state.todos and state.logging). This is useful when creating a root reducer.
  • reduceReducers - each reducer manage the same state. This is useful when chaining several reducers which are supposed to operate over the same state (this might happen for example when combining several reducer created using handleAction from redux-actions)

The difference is obvious from the final state shape.

Seth
  • 5,489
  • 4
  • 40
  • 51
Tomáš Ehrlich
  • 5,738
  • 2
  • 21
  • 30
  • According to https://github.com/redux-utilities/reduce-reducers/releases, the initial state problem has been resolved. Can you confirm, @tomáš-ehrlich? – Seth May 26 '20 at 02:11
  • 1
    @Seth I can't, unfortunately. I don't work on any project which still uses Redux. If it's fixed, you want me to remove the `Caveat` paragraph from the answer? – Tomáš Ehrlich May 26 '20 at 16:17
0

I also don't get what reduce-reducers is trying to solve. The use case described by @Tomáš can be achieved by a simple Reducer. After all, Reducer is just a function that accepts app-state and an action, and returns an object containing the new app-state. For instance, you can do the following instead of using the provided combineReducers by redux:

import combinationReducer from "./combinationReducer";
import endOfPlayReducer from "./endOfPlayReducer";
import feedbackReducer from "./feedbackReducer";

function combineReducers(appState, action) {
  return {
    combination: combinationReducer(appState, action),
    feedbacks: feedbackReducer(appState, action),
    endOfPlay: endOfPlayReducer(appState, action)
  };
}

And of course here, your reducers are accepting the whole app-state and returning only the slice they are responsible for. Again, it's just a function, you can customise it anyway you like. You can read more about it here

Behnam Rasooli
  • 668
  • 5
  • 19
  • 1
    You miss out on some optimization afaik. For example, I think React uses `Object.is()` or `===` to compare state, and returning a new state every time might cause a lot of re-rendering. – jchook Sep 13 '19 at 00:38
  • 1
    You're creating a nested structure. reduce-reducers isn't for replacing combineReducers, it's for having two reducers operate on the same slice of state. You might have a library that supplies a reducer out of the box, but you also have a custom reducer you want to operate on that same slice of state. You'd use reducerReducers to combine both of those reducers. – duhseekoh Oct 18 '19 at 21:06