更新 Redux 状态的多个部分的模式

Pattern for updating multiple parts of Redux state

我的 Redux 状态的形状如下所示:

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

使用 combineReducers 我有 2 套减速器。每个都作用于状态的根键之一。即一个管理 user 键,另一个管理 items 键。

如果我想添加一个项目,我可以调用 2 个 reducer,第一个会向 items 添加一个新对象,第二个会将 id 添加到 user.items 数组。

这有不好的代码味道。我觉得应该有一种方法可以同时原子地减少两个对象的状态。即除了子减速器之外,还有一个作用于根对象的根减速器。这可能吗?

我觉得你做的其实是对的!

调度action时,从root-reducer开始,每"sub-reducer"都会调用一次,将对应的"sub-state"和action传递给sub-reducers的下一层。您可能认为这不是一个好的模式,因为每个 "sub-reducer" 都会被调用并一直向下传播到状态树的每个叶节点,但实际上并非如此!

如果动作定义在switch case中,"sub-reducer"只会改变它拥有的"sub-state"部分,并可能将动作传递给下一层,但如果动作不是' t 在 "sub-reducer" 中定义,它什么都不做,return 当前 "sub-state",它停止传播。

让我们看一个更复杂的状态树的例子!


假设您使用 redux-simple-router,我将您的情况扩展为更复杂(具有多个用户的数据),那么您的状态树可能如下所示:

{
  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
  }
}

如你所见,状态树中有嵌套层,为了处理这个我们使用reducer composition,其概念是使用combineReducer()用于状态树中的每一层。

所以你的减速器应该看起来像这样: (为了说明层层概念,这里是outside-in,所以顺序倒过来了)

第一层:

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'
})

第二层(实体部分):

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
})

第三层(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:
  }
}

当动作调度时

redux 将调用每个 sub-reducer 来自 rootReducer,
通过:
currentUser: {...} sub-state 整个动作到 currentUserReducer
entities: {users: {...}, items: {...}} 和对 entitiesReducer
的操作 routing: {...} 和对 routeReducer
的操作 和...
entitiesReducer 会将 users: {...} 和操作传递给 usersReducer,
items: {...} 和对 itemsReducer

的操作

为什么这么好?

所以你提到有没有办法让 root reducer 处理状态的不同部分,而不是将它们传递给单独的 sub-reducers。但是如果你不使用 reducer 组合并编写一个巨大的 reducer 来处理状态的每个部分,或者你只是将你的状态嵌套到一个深度嵌套的树中,那么随着你的应用程序变得越来越复杂(假设每个用户都有一个 [friends] 数组,或者项目可以有 [tags],等等),如果不是不可能找出每种情况,那将是非常复杂的。

此外,拆分 reducer 使您的应用程序非常灵活,您只需将任何 case TYPE_NAME 添加到 reducer 以对该操作做出反应(只要您的父 reducer 将其传递下去)。

例如,如果你想跟踪用户是否访问了某条路线,只需将 case UPDATE_PATH 添加到你的 reducer 开关中即可!