Перейти к основному содержимому

Основы Redux, Часть 3: Состояние, Действия и Редьюсеры

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Что вы изучите
  • Как определять значения состояния, содержащие данные вашего приложения
  • Как определять объекты действий, описывающие происходящее в приложении
  • Как писать редьюсеры для вычисления обновлённого состояния на основе текущего состояния и действий
Предварительные требования

Введение

В Части 2: Концепции Redux и поток данных мы рассмотрели, как Redux помогает создавать поддерживаемые приложения, предоставляя единое централизованное место для глобального состояния. Мы также обсудили основные концепции Redux: диспетчеризацию объектов действий и использование редьюсеров для возврата новых значений состояния.

Теперь, когда вы понимаете эти компоненты, пора применить знания на практике. Мы создадим небольшое примерное приложение, чтобы увидеть, как эти части работают вместе.

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Внимание!

Обратите внимание, что в этом руководстве намеренно используются устаревшие шаблоны логики Redux, требующие больше кода, чем "современные Redux" шаблоны с Redux Toolkit, которые мы рекомендуем как правильный подход для создания приложений на Redux сегодня. Это сделано для объяснения принципов и концепций Redux. Данный материал не предназначен для использования в production-проектах.

Чтобы изучить "современный Redux" с Redux Toolkit, перейдите по ссылкам:

Настройка проекта

Для этого руководства мы подготовили стартовый проект с предварительной настройкой: React уже настроен, добавлены стили по умолчанию и реализовано фейковое REST API для работы с реальными HTTP-запросами. Вы будете использовать его как основу для написания кода приложения.

Для начала откройте и форкните этот CodeSandbox:

Вы также можете клонировать этот проект из GitHub-репозитория. После клонирования установите зависимости командой npm install и запустите проект командой npm start.

Готовую версию приложения можно посмотреть в ветке tutorial-steps или в этом CodeSandbox.

Создание нового проекта на Redux + React

После прохождения руководства вы сможете создавать собственные проекты. Для быстрого старта мы рекомендуем использовать Redux-шаблоны для Create-React-App. В них уже настроены Redux Toolkit и React-Redux на основе модернизированного примера счётчика из Части 1, что позволяет сразу приступить к написанию кода без ручной настройки хранилища.

Детали подключения Redux к проекту:

Detailed Explanation: Adding Redux to a React Project

The Redux template for CRA comes with Redux Toolkit and React-Redux already configured. If you're setting up a new project from scratch without that template, follow these steps:

  • Add the @reduxjs/toolkit and react-redux packages
  • Create a Redux store using RTK's configureStore API, and pass in at least one reducer function
  • Import the Redux store into your application's entry point file (such as src/index.js)
  • Wrap your root React component with the <Provider> component from React-Redux, like:
root.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

Изучение стартового проекта

Этот стартовый проект основан на стандартном шаблоне Vite с некоторыми изменениями.

Давайте кратко рассмотрим содержимое стартового проекта:

  • /src
    • index.js: точка входа приложения. Рендерит основной компонент <App>
    • App.js: главный компонент приложения
    • index.css: глобальные стили приложения
    • /api
      • client.js: обёртка над fetch для выполнения HTTP GET/POST запросов
      • server.js: фейковое REST API. Позже приложение будет получать данные отсюда
    • /exampleAddons: дополнительные плагины Redux для демонстрации в руководстве

При запуске приложения вы увидите приветствие, но остальной интерфейс будет пустым.

Итак, давайте начнём!

Начало работы над приложением Todo

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

Определение требований

Начнём с формулировки начальных бизнес-требований для этого приложения:

  • Интерфейс должен состоять из трёх основных разделов:

    • Поле ввода для добавления новых задач
    • Список существующих задач
    • Нижний раздел с количеством невыполненных задач и фильтрами
  • Элементы списка должны иметь:

    • Чекбокс для отметки выполнения
    • Возможность добавления цветовой категории из предопределённого списка
    • Кнопку удаления задачи
  • Счётчик должен правильно склонять количество активных задач: "0 задач", "1 задача", "3 задачи" и т.д.

  • Должны быть кнопки для:

    • Отметки всех задач как выполненных
    • Очистки выполненных задач
  • Фильтрация задач должна поддерживать два варианта:

    • По статусу выполнения: "Все", "Активные", "Завершённые"
    • По цветовым меткам: выбор одной или нескольких категорий

Позже мы добавим дополнительные требования, но этого достаточно для начала.

Конечная цель — приложение, которое выглядит так:

Скриншот примера todo-приложения

Проектирование состояния

Один из основных принципов React и Redux: ваш интерфейс должен основываться на состоянии. Поэтому при проектировании приложения сначала продумайте всё состояние, необходимое для описания его работы. Также рекомендуется описывать UI минимальным набором значений состояния, чтобы упростить отслеживание и обновление данных.

Концептуально приложение состоит из двух основных аспектов:

  • Текущий список задач

  • Параметры фильтрации

Дополнительно нам понадобится отслеживать данные, которые пользователь вводит в поле "Добавить задачу", но это менее важно и мы рассмотрим это позже.

Для каждой задачи нам нужно хранить несколько элементов данных:

  • Текст задачи

  • Флаг выполнения (булево значение)

  • Уникальный идентификатор

  • Цветовая категория (если выбрана)

Поведение фильтров можно описать с помощью перечисляемых значений:

  • Статус выполнения: "Все", "Активные", "Завершённые"

  • Цвета: "Красный", "Жёлтый", "Зелёный", "Синий", "Оранжевый", "Фиолетовый"

Эти значения можно разделить на:

  • "Состояние приложения" (основные данные)
  • "Состояние UI" (параметры текущего отображения) Такое разделение помогает понять, как используются разные части состояния.

Структура состояния

В Redux состояние приложения всегда хранится в виде простых объектов и массивов JavaScript. Это означает, что в состояние Redux нельзя помещать классы, встроенные типы JS, такие как Map / Set / Promise / Date, функции или другие непростые JS-данные.

Корневое состояние Redux почти всегда представляет собой простой JS-объект с вложенными данными.

На основе этой информации мы можем описать типы значений, которые должны храниться в состоянии Redux:

  • Во-первых, нам нужен массив объектов задач. Каждый элемент должен содержать поля:

    • id: уникальный числовой идентификатор
    • text: текст, введённый пользователем
    • completed: флаг завершения (логическое значение)
    • color: необязательная цветовая категория
  • Далее нам необходимо определить параметры фильтрации:

    • Текущее значение фильтра по статусу выполнения
    • Массив выбранных цветовых категорий

Итак, вот как может выглядеть пример состояния нашего приложения:

const todoAppState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'Active',
colors: ['red', 'blue']
}
}

Пример состояния нашего приложения может выглядеть так:

Проектирование действий (Actions)

Действия (Actions) — это простые JavaScript-объекты с полем type. Как упоминалось ранее, действие можно рассматривать как событие, описывающее произошедшее в приложении.

Подобно проектированию структуры состояния, мы можем определить список действий, соответствующих функциональности приложения:

  • Добавление новой задачи на основе введённого текста

  • Переключение статуса выполнения задачи

  • Выбор цветовой категории для задачи

  • Удаление задачи

  • Отметка всех задач как выполненных

  • Очистка выполненных задач

  • Изменение значения фильтра по статусу выполнения

  • Добавление нового цветового фильтра

  • Удаление цветового фильтра

Дополнительные данные, описывающие событие, обычно помещаются в поле action.payload. Это может быть число, строка или объект с несколькими полями.

Хранилищу Redux не важно конкретное значение action.type, но ваш код будет проверять action.type, чтобы определить необходимость обновления. При отладке в Redux DevTools вы часто будете видеть типы действий, поэтому выбирайте читаемые значения, однозначно описывающие событие — это упростит понимание логики приложения.

На основе списка событий определим действия для нашего приложения:

  • {type: 'todos/todoAdded', payload: todoText}

  • {type: 'todos/todoToggled', payload: todoId}

  • {type: 'todos/colorSelected', payload: {todoId, color}}

  • {type: 'todos/todoDeleted', payload: todoId}

  • {type: 'todos/allCompleted'}

  • {type: 'todos/completedCleared'}

  • {type: 'filters/statusFilterChanged', payload: filterValue}

  • {type: 'filters/colorFilterChanged', payload: {color, changeType}}

Большинство действий содержат одно дополнительное значение, поэтому мы используем action.payload. Для цветового фильтра мы сознательно выбрали единое действие с объектом-параметром, чтобы продемонстрировать возможность сложных полезных нагрузок.

Как и данные состояния, действия должны содержать минимально необходимую информацию для описания события.

Написание редьюсеров

Теперь, когда мы знаем структуру состояния и вид действий, пришло время написать наш первый редьюсер.

Редьюсеры — это функции, которые принимают текущее state и action в качестве аргументов и возвращают новое state. Иными словами, (state, action) => newState.

Создание корневого редьюсера

В приложении Redux действительно есть только одна функция-редьюсер: "корневой редьюсер", который позже будет передан в createStore. Этот единственный корневой редьюсер отвечает за обработку всех диспатчимых действий и вычисление целиком нового состояния при каждом обновлении.

Давайте начнём с создания файла reducer.js в папке src, рядом с index.js и App.js.

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

src/reducer.js
const initialState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'All',
colors: []
}
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

Редьюсер может быть вызван со значением undefined в качестве состояния при инициализации приложения. В этом случае нужно предоставить начальное состояние, чтобы остальной код редьюсера мог работать. Обычно редьюсеры используют синтаксис аргументов по умолчанию для задания начального состояния: (state = initialState, action).

Далее добавим логику для обработки действия 'todos/todoAdded'.

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

src/reducer.js
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
case 'todos/todoAdded': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new array for the `todos` field
todos: [
// with all of the old todos
...state.todos,
// and the new todo object
{
// Use an auto-incrementing numeric ID for this example
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

Это... очень много работы для добавления одной задачи в состояние. Зачем нужны все эти дополнительные шаги?

Правила редьюсеров

Ранее мы упоминали, что редьюсеры обязаны следовать особым правилам:

  • Они должны вычислять новое значение состояния только на основе аргументов state и action

  • Им запрещено изменять существующий state. Вместо этого они должны выполнять неизменяемые обновления, копируя существующий state и внося изменения в скопированные значения.

  • Они не должны выполнять асинхронную логику или другие "побочные эффекты"

Совет

"Побочный эффект" — это любое изменение состояния или поведения, которое можно наблюдать вне возврата значения из функции. Распространённые примеры побочных эффектов:

  • Вывод значения в консоль
  • Сохранение файла
  • Установка асинхронного таймера
  • Выполнение HTTP-запроса
  • Изменение состояния вне функции или мутация аргументов функции
  • Генерация случайных чисел или уникальных ID (например, Math.random() или Date.now())

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

Но почему эти правила важны? Есть несколько причин:

Но почему эти правила важны? Есть несколько причин:

  • Одна из целей Redux — сделать ваш код предсказуемым. Когда результат функции вычисляется только на основе входных аргументов, легче понять, как работает этот код, и протестировать его.

  • С другой стороны, если функция зависит от внешних переменных или ведёт себя случайным образом, вы никогда не сможете предсказать результат её выполнения.

  • Если функция изменяет другие значения, включая свои аргументы, это может неожиданно повлиять на работу приложения. Часто это приводит к ошибкам вроде: "Я обновил состояние, но интерфейс не перерисовывается!"

  • Некоторые возможности Redux DevTools работают корректно только при соблюдении этих правил в редьюсерах

Особенно важно правило "иммутабельных обновлений", которое заслуживает отдельного обсуждения.

Редьюсеры и иммутабельные обновления

Ранее мы обсуждали "мутацию" (изменение существующих объектов/массивов) и "иммутабельность" (трактовку значений как неизменяемых).

Предупреждение

В Redux редьюсерам запрещено изменять оригинальные/текущие значения состояния!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

Запрет на мутацию состояния в Redux обусловлен несколькими причинами:

  • Это вызывает ошибки, например, интерфейс не обновляется при изменении значений

  • Усложняет понимание причин и механизмов обновления состояния

  • Затрудняет написание тестов

  • Ломает функциональность "отладки с перемещением во времени"

  • Противоречит философии и паттернам использования Redux

Если нельзя изменять оригиналы, как тогда возвращать обновлённое состояние?

Совет

Редьюсеры могут создавать копии исходных значений и изменять эти копии.

// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}

Мы уже видели, что можем писать иммутабельные обновления вручную, используя операторы расширения JavaScript для массивов/объектов и другие функции, возвращающие копии исходных значений.

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

Однако, если вам кажется, что "писать иммутабельные обновления вручную сложно и легко ошибиться"... вы абсолютно правы! :)

Ручное написание логики иммутабельных обновлений действительно сложно, и случайная мутация состояния в редьюсерах — самая распространённая ошибка пользователей Redux.

Совет

В реальных приложениях вам не придётся вручную писать такие сложные вложенные иммутабельные обновления. В Части 8: Современный Redux с Redux Toolkit вы научитесь использовать Redux Toolkit для упрощения написания иммутабельной логики обновлений в редьюсерах.

Обработка дополнительных действий

Учитывая это, добавим логику редьюсера для ещё нескольких случаев. Сначала — переключение поля completed у задачи по её ID:

src/reducer.js
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
// Again copy the entire state object
...state,
// This time, we need to make a copy of the old todos array
todos: state.todos.map(todo => {
// If this isn't the todo item we're looking for, leave it alone
if (todo.id !== action.payload) {
return todo
}

// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
})
}
}
default:
return state
}
}

И поскольку мы сосредоточились на состоянии задач, добавим обработку действия "изменён фильтр видимости":

src/reducer.js
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
...state,
todos: state.todos.map(todo => {
if (todo.id !== action.payload) {
return todo
}

return {
...todo,
completed: !todo.completed
}
})
}
}
case 'filters/statusFilterChanged': {
return {
// Copy the whole state
...state,
// Overwrite the filters value
filters: {
// copy the other filter fields
...state.filters,
// And replace the status field with the new value
status: action.payload
}
}
}
default:
return state
}
}

Мы обработали всего 3 действия, но код уже становится громоздким. Если продолжать обрабатывать все действия в одной функции редьюсера, читать её будет сложно.

Поэтому редьюсеры обычно разделяют на несколько меньших функций — чтобы упростить понимание и поддержку логики.

Разделение редьюсеров

Часто редьюсеры Redux разделяют по разделам состояния, которые они обновляют. Наше состояние приложения сейчас имеет два основных раздела: state.todos и state.filters. Поэтому мы можем разделить главный редьюсер на два меньших — todosReducer и filtersReducer.

Где должны находиться эти разделённые функции?

Мы рекомендуем организовывать папки и файлы Redux-приложения по "фичам" — коду, относящемуся к конкретной концепции или области приложения. Код Redux для фичи обычно пишется в одном файле, называемом "слайсом", который содержит всю логику редьюсера и связанные с действиями коды для этой части состояния.

Поэтому редьюсер для конкретного раздела состояния называется "слайс-редьюсером". Обычно некоторые объекты действий тесно связаны с определённым слайс-редьюсером, и их типы должны начинаться с имени фичи (например, 'todos') и описывать произошедшее событие (например, 'todoAdded'), объединённые в одну строку ('todos/todoAdded').

В нашем проекте создадим папку features, а внутри неё — папку todos. Создадим файл todosSlice.js и перенесём в него начальное состояние для задач:

src/features/todos/todosSlice.js
const initialState = [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
]

function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}

export default function todosReducer(state = initialState, action) {
switch (action.type) {
default:
return state
}
}

Теперь скопируем логику обновления задач. Но есть важное отличие: этот файл обновляет только состояние задач — оно больше не вложено! Это ещё одна причина разделения редьюсеров. Поскольку состояние задач теперь самостоятельный массив, нам не нужно копировать внешний корневой объект состояния. Это упрощает чтение редьюсера.

Это называется композицией редьюсеров — фундаментальным паттерном построения Redux-приложений.

Вот как выглядит обновлённый редьюсер после обработки действий:

src/features/todos/todosSlice.js
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Can return just the new todos array - no extra object around it
return [
...state,
{
id: nextTodoId(state),
text: action.payload,
completed: false
}
]
}
case 'todos/todoToggled': {
return state.map(todo => {
if (todo.id !== action.payload) {
return todo
}

return {
...todo,
completed: !todo.completed
}
})
}
default:
return state
}
}

Теперь код стал короче и понятнее.

Проделаем то же самое для логики фильтров. Создадим src/features/filters/filtersSlice.js и перенесём весь связанный с фильтрами код:

src/features/filters/filtersSlice.js
const initialState = {
status: 'All',
colors: []
}

export default function filtersReducer(state = initialState, action) {
switch (action.type) {
case 'filters/statusFilterChanged': {
return {
// Again, one less level of nesting to copy
...state,
status: action.payload
}
}
default:
return state
}
}

Нам всё ещё нужно копировать объект состояния фильтров, но благодаря меньшей вложенности происходящее легче понять.

Информация

Чтобы не перегружать страницу, мы пропустим демонстрацию логики редьюсера для других действий.

Попробуйте самостоятельно реализовать эти обновления, руководствуясь описанными выше требованиями.

Если возникнут трудности, обратитесь к финальному CodeSandbox для полной реализации этих редьюсеров.

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

Теперь у нас есть два отдельных файла со срезами (slice), каждый со своей функцией-редьюсером. Но ранее мы говорили, что хранилищу Redux требуется один корневой редьюсер при создании. Как же вернуться к единому корневому редьюсеру, не объединяя весь код в одну громоздкую функцию?

Поскольку редьюсеры — это обычные JS-функции, мы можем импортировать их обратно в reducer.js и создать новый корневой редьюсер, чья единственная задача — вызывать остальные две функции.

src/reducer.js
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

export default function rootReducer(state = {}, action) {
// always return a new object for the root state
return {
// the value of `state.todos` is whatever the todos reducer returns
todos: todosReducer(state.todos, action),
// For both reducers, we only pass in their slice of the state
filters: filtersReducer(state.filters, action)
}
}

Обратите внимание: каждый из этих редьюсеров управляет своей частью глобального состояния. Параметр state уникален для каждого редьюсера и соответствует контролируемой им части состояния.

Это позволяет разделять логику по функциональности и срезам состояния для поддержания читаемости кода.

combineReducers

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

Библиотека Redux включает утилиту combineReducers, которая автоматизирует эту шаблонную операцию. Мы можем заменить наш "ручной" rootReducer на более лаконичную версию, сгенерированную combineReducers.

Теперь, когда нам нужен combineReducers, пора установить основную библиотеку Redux:

npm install redux

После установки мы можем импортировать combineReducers и использовать его:

src/reducer.js
import { combineReducers } from 'redux'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
})

export default rootReducer

combineReducers принимает объект, где ключи станут ключами вашего корневого состояния, а значения — функциями-редьюсерами срезов, которые знают, как обновлять соответствующие части состояния Redux.

Помните: имена ключей, передаваемые в combineReducers, определяют структуру вашего корневого объекта состояния!

Итоги изученного

Состояние, действия и редьюсеры — это строительные блоки Redux. Каждое приложение на Redux содержит значения состояния, создаёт действия для описания событий и использует функции-редьюсеры для вычисления новых значений на основе предыдущего состояния и действия.

Вот текущее состояние нашего приложения:

Итоги
  • Приложения Redux используют обычные JS-объекты, массивы и примитивы в качестве значений состояния
    • Корневое состояние должно быть простым JS-объектом
    • Состояние должно содержать минимально необходимый объём данных
    • Классы, промисы, функции и другие не-примитивные значения не должны попадать в состояние Redux
    • Редьюсеры не должны создавать случайные значения вроде Math.random() или Date.now()
    • Допустимо иметь другие значения состояния вне хранилища Redux (например, локальное состояние компонентов) параллельно с Redux
  • Действия — это простые объекты с полем type, описывающим произошедшее событие
    • Поле type должно быть читаемой строкой, обычно в формате 'feature/eventName'
    • Действия могут содержать дополнительные данные, обычно хранящиеся в поле action.payload
    • Действия должны содержать минимально необходимый объём данных
  • Редьюсеры — это функции вида (state, action) => newState
    • Редьюсеры всегда должны соблюдать правила:
      • Вычислять новое состояние исключительно на основе state и action
      • Никогда не изменять существующий state — всегда возвращать копию
      • Не содержать побочных эффектов (HTTP-запросы, асинхронная логика)
  • Редьюсеры следует разделять для удобства чтения
    • Разделение обычно происходит по ключам верхнего уровня или "срезам" состояния
    • Редьюсеры обычно размещаются в файлах срезов, сгруппированных по функциональности
    • Редьюсеры можно комбинировать с помощью функции Redux combineReducers
    • Ключи, передаваемые в combineReducers, определяют структуру корневого состояния

Что дальше?

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

В Части 4: Хранилище мы увидим, как создать хранилище Redux и запустить нашу логику редьюсеров.