Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern for updating multiple parts of Redux state

The shape of my Redux state looks like this:

{
  user: {
    id: 123,
    items: [1, 2]
  },
  items: {
    1: {
      ...
    },
    2: {
      ...
    }
  }
}

Using combineReducers I have 2 sets of reducers. Each act on one of the root keys of the state. i.e. one manages the user key and the other the items key.

If I want to add an item I can call 2 reducers, the first will add a new object to the items and the second will add the id to the user.items array.

This has a bad code smell. I feel that there should be a way to atomically reduce the state of both objects at the same time. i.e. in addition to the sub-reducers have a root reducer that acts on the root object. Is this possible?

like image 951
Guy Avatar asked Dec 30 '15 14:12

Guy


1 Answers

I think what you're doing is actually correct!

When dispatching an action, starting from the root-reducer, every "sub-reducer" will be called, passing the corresponding "sub-state" and action to the next layer of sub-reducers. You might think that this is not a good pattern since every "sub-reducer" gets called and propagates all the way down to every single leaf node of the state tree, but this is actually not the case!

If the action is defined in the switch case, the "sub-reducer" will only change the "sub-state" part it owns, and maybe passes the action to the next layer, but if the action isn't defined in the "sub-reducer", it will do nothing and return the current "sub-state", which stops the propagation.

Let's see an example with a more complex state tree!


Say you use redux-simple-router, and I extended your case to be more complex (having data of multiple users), then your state tree might look something like this:

{
  currentUser: {
    loggedIn: true,
    id: 123,
  },
  entities: {
    users: {
      123: {
        id: 123,
        items: [1, 2]
      },
      456: {
        id: 456,
        items: [...]
      }
    },
    items: {
      1: {
        ...
      },
      2: {
        ...
      }
    }
  },
  routing: {
    changeId: 3,
    path: "/",
    state: undefined,
    replace:false
  }
}

As you can see already, there are nested layers in the state tree, and to deal with this we use reducer composition, and the concept is to use combineReducer() for every layer in the state tree.

So your reducer should look something like this: (To illustrate the layer by layer concept, this is outside-in, so the order is backwards)

first layer:

import { routeReducer } from 'redux-simple-router'

function currentUserReducer(state = {}, action) {
  switch (action.type) {...}
}

const rootReducer = combineReducers({
  currentUser: currentUserReducer,
  entities: entitiesReducer, // from the second layer
  routing: routeReducer      // from 'redux-simple-router'
})

second layer (the entities part):

function usersReducer(state = {}, action) {
  switch (action.type) {
  case ADD_ITEM:
  case TYPE_TWO:
  case TYPE_TREE:
    return Object.assign({}, state, {
      // you can think of this as passing it to the "third layer"
      [action.userId]: itemsInUserReducer(state[action.userId], action)
    })
  case TYPE_FOUR:
    return ...
  default:
    return state
  }
}

function itemsReducer(...) {...}

const entitiesReducer = combineReducers({
  users: usersReducer,
  items: itemsReducer
})

third layer (entities.users.items):

/**
 * Note: only ADD_ITEM, TYPE_TWO, TYPE_TREE will be called here,
 *       no other types will propagate to this reducer
 */
function itemsInUserReducer(state = {}, action) {
  switch (action.type) {
  case ADD_ITEM:
    return Object.assign({}, state, {
      items: state.items.concat([action.itemId])
      // or items: [...state.items, action.itemId]
    })
  case TYPE_TWO:
    return DO_SOMETHING
  case TYPE_TREE:
    return DO_SOMETHING_ELSE
  default:
    state:
  }
}

when an action dispatches

redux will call every sub-reducer from the rootReducer,
passing:
currentUser: {...} sub-state and the whole action to currentUserReducer
entities: {users: {...}, items: {...}} and action to entitiesReducer
routing: {...} and action to routeReducer
and...
entitiesReducer will pass users: {...} and action to usersReducer,
and items: {...} and action to itemsReducer

why is this good?

So you mentioned is there a way to have the root reducer handling different parts of the state, instead of passing them to separate sub-reducers. But if you don't use reducer composition and write a huge reducer to handle every part of the state, or you simply nest you state into a deeply nested tree, then as your app gets more complicated (say every user has a [friends] array, or items can have [tags], etc), it will be insanely complicated if not impossible to figure out every case.

Furthermore, splitting reducers makes your app extremely flexible, you just have to add any case TYPE_NAME to a reducer to react to that action (as long as your parent reducer passes it down).

For example if you want to track if the user visits some route, just add the case UPDATE_PATH to your reducer switch!

like image 121
DaxChen Avatar answered Sep 22 '22 22:09

DaxChen