user2331095
user2331095

Reputation: 6707

Can combineReducers work with extra actions?

What I want is the root reducer combine other reducers, and listen to extra actions. I've find the docs, but I can not get any information.

Here is some pseudo code.

const root1 = combineReducers({
  reducer1,
  reducer2,
  reducer3,
  reducer4
});

function root2(state = initState, action) {
  switch (action.type) {
    case LOAD_DATA:
      return _.assign({}, initState, action.data);
    default:
      return state;
  }
}

merge(root1, root2);

The only way I figure out is to drop combineReducers:

function root(state = initState, action) {
  switch (action.type) {
    case LOAD_DATA:
      return _.assign({}, initState, action.data);
    case ...: return ...;
    case ...: return ...;
    case ...: return ...;
    default: return state;  
  }
}

Is there another way to implement this?

Upvotes: 3

Views: 1790

Answers (2)

markerikson
markerikson

Reputation: 67439

First, combineReducers is merely a utility function that simplifies the common use case of "this reducer function should handle updates to this subset of data". It's not required.

Second, that looks like pretty much the exact use case for https://github.com/acdlite/reduce-reducers. There's an example here: https://github.com/reactjs/redux/issues/749#issuecomment-164327121

export default reduceReducers(
  combineReducers({
    router: routerReducer,
    customers,
    stats,
    dates,
    filters,
    ui
  }),
  // cross-cutting concerns because here `state` is the whole state tree
  (state, action) => {
    switch (action.type) {
      case 'SOME_ACTION':
        const customers = state.customers;
        const filters = state.filters;
        // ... do stuff
    }
  }
);

Also, I give an example of using reduceReducers in the "Structuring Reducers" section of the Redux docs: http://redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html .

Upvotes: 2

Andy Noelker
Andy Noelker

Reputation: 11269

Yes, you can use combineReducers() with multiple reducers while also having an action that rebuilds your entire application state. Admittedly, that is a bit of a strange design decision and does not scale very well with more complex apps, but you obviously have a use-case. If you want to do something like that you have two choices.

Option 1: Divide up action

It is totally valid to listen for the same action type within multiple reducer functions. This is the most straightforward approach, although it involves more repetition. You would just break out each piece of state returned by your action into the individual reducer functions it applies to.

For instance, if this was your entire application state

{
    foo: {},
    bar: {}
}

And your action type that rebuilt the entire application state was LOAD_DATA, you could do this

function foo (state = {}, action) {
    switch (action.type) {
        case 'LOAD_DATA':
            return {...state, action.result.foo}
    }
}

function bar (state = {}, action) {
    switch (action.type) {
        case 'LOAD_DATA':
            return {...state, action.result.bar}
    }
}

const root = combineReducers({
  foo,
  bar
});

With that, both foo and bar in your state would always get rebuilt with the corresponding data coming from the same action.

Option 2: Build Custom combineReducers()

There is nothing stopping you from building your own version of combineReducers(). If you watch this video on building a combineReducers() function from scratch, you'll see that the logic in place is not that complicated. You would just have to listen for the specific action type and return the entire state from that action if it matched. Here's a version of that I built by looking at the current source for combineReducers() and then working the 2 util functions into that function

function combineReducers(reducers) {
  var fn = (val) => typeof val === 'function';
  var finalReducers = Object.keys(reducers).reduce((result, key) => {
    if (fn(reducers[key])) {
      result[key] = reducers[key]
    }
    return result
  }, {});

  return function combination(state = {}, action) {
    if (action.type === 'LOAD_DATA') {
        return completeStateReducer(action)
    } else {
        var hasChanged = false
        var fn = (reducer, key) => {
          var previousStateForKey = state[key]
          var nextStateForKey = reducer(previousStateForKey, action)
          if (typeof nextStateForKey === 'undefined') {
            var errorMessage = getUndefinedStateErrorMessage(key, action)
            throw new Error(errorMessage)
          }
          hasChanged = hasChanged || nextStateForKey !== previousStateForKey
          return nextStateForKey
        }
        var finalState = Object.keys(finalReducers).reduce((result, key) => {
            result[key] = fn(finalReducers[key], key)
            return result
          }, {})

        return hasChanged ? finalState : state
    }
  }
}

function completeStateReducer(action) {
    return action.result;
}

Outside of merging those util functions back in, the only thing I really added was the bit about listening for the LOAD_DATA action type and then calling completeStateReducer() when that happens instead of combining the other reducer functions. Of course, this assumes that your LOAD_DATA action actually returns your entire state, but even if it doesn't, this should point you in the right direction of building out your own solution.

Upvotes: 4

Related Questions