Все, что хранится в контейнере, мы называем состоянием, но не все состояния одинаково полезны. Вот какую классификацию вводит документация Redux:

  • Domain data — данные приложения, которые нужно отображать, использовать и модифицировать. Например, список пользователей, загруженный с сервера.
  • App state — данные, определяющие поведение приложения. Например, текущий открытый URL.
  • UI state — данные, определяющие то, как выглядит UI. Например, вывод списка в плиточном виде.

Так как контейнер представляет собой ядро приложения, данные внутри него, должны описываться в терминах domain data и app state, но не как дерево компонентов UI. Например, такой способ формирования состояния state.leftPane.todoList.todos — плохая идея. Крайне редко дерево компонентов отражается напрямую на структуру состояния, и это нормально. Представление зависит от данных, а не данные от представления.

Типичная структура состояния выглядит так:

{
    domainData1 : {}, // todos
    domainData2 : {}, // comments
    appState1 : {},
    appState2 : {},
    uiState1 : {}
    uiState2 : {},
}

Подробнее про работу с состоянием UI будет рассказано в соответствующем уроке.

Как уже говорилось в курсе JS: React, структура состояния должна напоминать базу данных. Всё максимально плоско и нормализованно.

{
  todos: [
    { id: 1, name: 'why?' },
    { id: 3, name: 'who?' },
  ],
  comments: [
    { id: 23, todoId: 3, text: 'great!' },
  ],
}

С такой структурой крайне легко писать реакцию на действия, обновлять данные, добавлять новые и удалять старые. Вложенность небольшая, всё просто. Но появляется другая проблема (появляется она в любом случае). С ростом количества сущностей, редьюсер становится очень тяжёлым. Огромный кусок кода, который делает всё.

Redux имеет встроенный механизм, позволяющий создавать множественные редьюсеры и комбинировать их друг с другом. Работает это так: для каждого свойства верхнего уровня пишется свой собственный редьюсер, а затем они с помощью функции combineReducers объединяются в корневой (root) редьюсер, который уже используется для создания контейнера.

import { combineReducers, createStore } from 'redux';

const todos = (state = {}, action) => {
  // state is todos part
};

const comments = (state = {}, action) => {
  // state is comments
};

const rootReducer = combineReducers({ todos, comments });
const store = createStore(rootReducer);

Обратите внимание на то, что если редьюсер именовать так же, как и свойство, то можно написать так: { todos, comments }. В каждый редьюсер приходит state, но это не всё состояние контейнера, а только та часть, которая лежит в соответствующем свойстве. Не забудьте про это.

Комбинация редьюсеров
Redux

Редьюсеры могут быть даже вложенными и для этого не нужны никакие специальные средства, обычные функции, принимающие на вход данные и возвращающие новые данные.

С таким подходом появляется одна особенность, которая, по началу, может испугать. Так как каждый редьюсер имеет доступ только к своей части состояния, действия, порождающие изменения сразу в нескольких местах, будут повторяться в разных редьюсерах:

const todos = (state = {}, action) => {
  switch (action.type) {
    case 'TODO_REMOVE':
      // ...
  }
};

const comments = (state = {}, action) => {
  switch (action.type) {
    // При удалении ToDo нужно удалить все его комментарии
    case 'TODO_REMOVE':
      // ...
  }
};

То есть правильный подход состоит в том, чтобы повторять часть case в нужных редьюсерах, а не в том, чтобы пытаться получить недостающие части состояния.


Дополнительные материалы

  1. Нормализация структуры состояния
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →