Pattern for updating multiple parts of Redux state

前端 未结 1 788
無奈伤痛
無奈伤痛 2021-02-04 05:44

The shape of my Redux state looks like this:

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


        
相关标签:
1条回答
  • 2021-02-04 06:23

    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!

    0 讨论(0)
提交回复
热议问题