Шаблон для обновления нескольких частей состояния Redux

Форма моего состояния Redux выглядит так:

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

Используя combReducers, у меня есть 2 набора редукторов. Каждое действие действует на один из корневых ключей состояния. т.е. один управляет ключом user а другой - клавишей items.

Если я хочу добавить элемент, я могу вызвать 2 редуктора, первый добавит новый объект к items а второй добавит id в массив user.items.

У этого плохой запах кода. Я чувствую, что должен быть способ атомизировать состояние обоих объектов одновременно. т.е. в дополнение к субредукторам есть редуктор корня, который действует на корневой объект. Это возможно?

Ответ 1

Я думаю, что вы делаете это правильно!

При отправке действия, начиная с корневого редуктора, вызывается каждый "вспомогательный редуктор", передавая соответствующее "под-состояние" и действие на следующий уровень вспомогательных редукторов. Вы можете подумать, что это не очень хороший шаблон, поскольку каждый "субредуктор" вызывается и распространяется до конца каждого отдельного листового узла дерева состояний, но на самом деле это не так!

Если действие определено в случае коммутатора, "субредуктор" будет изменять только часть "под-состояния", которой он владеет, и, возможно, передает действие на следующий уровень, но если действие не определено в " субредуктор ", он ничего не сделает и вернет текущее" под-состояние ", которое останавливает распространение.

Давайте посмотрим пример с более сложным деревом состояний!


Предположим, вы используете 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
  }
}

Как вы можете видеть, в дереве состояний есть вложенные слои, и для решения этой combineReducer() мы используем reducer composition, а концепция заключается в использовании combineReducer() для каждого уровня в дереве состояний.

Таким образом, ваш редуктор должен выглядеть примерно так: (Чтобы проиллюстрировать концепцию "слой за слоем", это внешний вид, поэтому порядок обратный)

первый слой:

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 будет вызывать каждый вспомогательный редуктор от rootReducer,
прохождение:
currentUser: {...} под-состояние и все действие to currentUserReducer
entities: {users: {...}, items: {...}} и действие для entitiesReducer
routing: {...} и действие для routeReducer
а также...
entitiesReducer будет передавать users: {...} и действие usersReducer,
и items: {...} и действие для itemsReducer

почему это хорошо?

Таким образом, вы упомянули, есть ли способ, чтобы корневой редуктор обрабатывал разные части состояния, вместо того, чтобы передавать их на отдельные субредукторы. Но если вы не используете композицию редуктора и напишите огромный редуктор, чтобы обрабатывать каждую часть состояния, или просто вставляете в глубокое вложенное дерево, тогда, когда ваше приложение становится более сложным (скажем, у каждого пользователя есть [friends] массив или элементы могут иметь [tags] и т.д.), это будет безумно сложным, если не невозможно разобраться в каждом случае.

Кроме того, сплит-редукторы делают ваше приложение чрезвычайно гибким, вам просто нужно добавить любой case TYPE_NAME к редуктору, чтобы реагировать на это действие (пока ваш родительский редуктор не передает его).

Например, если вы хотите отслеживать, посетил ли пользователь какой-либо маршрут, просто добавьте к case UPDATE_PATH случайный переключатель!