v19.2Latest

Вынос логики состояния в редьюсер

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

Вы узнаете
  • Что такое функция-редьюсер
  • Как рефакторитьuseState в useReducer
  • Когда использовать редьюсер
  • Как правильно его написать

Консолидация логики состояния с помощью редьюсера

По мере усложнения ваших компонентов становится труднее с первого взгляда увидеть все различные способы обновления состояния компонента. Например, компонентTaskAppниже хранит в состоянии массивtasksи использует три разных обработчика событий для добавления, удаления и редактирования задач:

Каждый из его обработчиков событий вызываетsetTasksдля обновления состояния. По мере роста этого компонента увеличивается и количество логики состояния, разбросанной по нему. Чтобы уменьшить эту сложность и держать всю логику в одном легкодоступном месте, вы можете переместить эту логику состояния в одну функцию вне вашего компонента,называемую «редьюсером».

Редьюсеры — это другой способ управления состоянием. Вы можете перейти отuseState к useReducerв три шага:

  1. Перейдитеот установки состояния к отправке действий.
  2. Напишитефункцию-редуктор.
  3. Используйтередьюсер из вашего компонента.

Шаг 1: Переход от установки состояния к отправке действий

Ваши обработчики событий в настоящее время указываютчто делатьпутем установки состояния:

Удалите всю логику установки состояния. У вас останутся три обработчика событий:

  • handleAddTask(text)вызывается, когда пользователь нажимает «Добавить».
  • handleChangeTask(task)вызывается, когда пользователь переключает задачу или нажимает «Сохранить».
  • handleDeleteTask(taskId)вызывается, когда пользователь нажимает «Удалить».

Управление состоянием с помощью редьюсеров немного отличается от прямой установки состояния. Вместо того чтобы указывать React «что делать», устанавливая состояние, вы указываете «что только что сделал пользователь», отправляя «действия» из ваших обработчиков событий. (Логика обновления состояния будет находиться в другом месте!) Таким образом, вместо «установкиtasks» через обработчик события вы отправляете действие «добавлена/изменена/удалена задача». Это лучше описывает намерение пользователя.

Объект, который вы передаете вdispatch, называется «действием»:

Это обычный объект JavaScript. Вы решаете, что в него поместить, но обычно он должен содержать минимальную информацию о том,что произошло. (Саму функциюdispatchвы добавите на следующем шаге.)

Примечание

Объект действия может иметь любую форму.

По соглашению, обычно ему задают строковыйtype, описывающий произошедшее, а любую дополнительную информацию передают в других полях.typeспецифичен для компонента, поэтому в этом примере подойдёт либо'added', либо'added_task'. Выберите имя, которое говорит о том, что произошло!

Шаг 2: Напишите функцию-редуктор

Функция-редуктор — это место, где будет находиться ваша логика состояния. Она принимает два аргумента: текущее состояние и объект действия, и возвращает следующее состояние:

React установит состояние равным тому, что вы вернёте из редюсера.

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

  1. Объявить текущее состояние (tasks) в качестве первого аргумента.
  2. Объявить объектactionв качестве второго аргумента.
  3. Вернутьследующеесостояние из редюсера (которое React установит в качестве состояния).

Вот вся логика установки состояния, перенесённая в функцию редюсера:

Поскольку функция редюсера принимает состояние (tasks) в качестве аргумента, вы можетеобъявить её вне вашего компонента.Это уменьшает уровень вложенности и может сделать ваш код более читаемым.

Примечание

В приведённом выше коде используются операторы if/else, но по соглашению внутри редьюсеров принято использоватьоператоры switch. Результат будет тем же, но операторы switch зачастую легче воспринимать с первого взгляда.

В оставшейся части документации мы будем использовать их следующим образом:

Мы рекомендуем заключать каждый блокcaseв фигурные скобки{ и }, чтобы переменные, объявленные внутри разныхcase, не конфликтовали друг с другом. Кроме того,caseобычно должен заканчиваться операторомreturn. Если вы забудете проreturn, код «провалится» в следующийcase, что может привести к ошибкам!

Если вы ещё не освоили операторы switch, использование if/else совершенно нормально.

Шаг 3: Использование редьюсера в вашем компоненте

Наконец, вам нужно подключитьtasksReducerк вашему компоненту. Импортируйте хукuseReducerиз React:

Затем вы можете заменитьuseState:

наuseReducerследующим образом:

ХукuseReducerпохож наuseState— вы должны передать ему начальное состояние, и он возвращает значение состояния и способ его обновления (в данном случае, функцию dispatch). Но есть небольшая разница.

ХукuseReducerпринимает два аргумента:

  1. Функцию-редуктор
  2. Начальное состояние

И возвращает:

  1. Значение состояния
  2. Функцию dispatch (для «отправки» пользовательских действий в редуктор)

Теперь всё подключено! Здесь редуктор объявлен внизу файла компонента:

При желании вы можете даже вынести редюсер в отдельный файл:

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

СравнениеuseState и useReducer

У редюсеров тоже есть недостатки! Вот несколько способов их сравнить:

  • Объём кода:Обычно сuseStateизначально нужно писать меньше кода. СuseReducerприходится писать и функцию-редьюсер,идействия для отправки. ОднакоuseReducerможет помочь сократить код, если множество обработчиков событий изменяют состояние схожим образом.
  • Читаемость:useStateочень легко читать, когда обновления состояния простые. Когда они становятся сложнее, они могут раздуть код компонента и затруднить его восприятие. В этом случаеuseReducerпозволяет чётко отделить логикукакобновления отчто произошлов обработчиках событий.
  • Отладка:При возникновении ошибки сuseStateбывает сложно определить,гдесостояние было установлено неверно, ипочему. СuseReducerможно добавить вывод в консоль в редьюсер, чтобы видеть каждое обновление состояния ипочемуоно произошло (из-за какогоaction). Если каждыйactionкорректен, вы поймёте, что ошибка в самой логике редьюсера. Однако придётся пройтись по большему количеству кода, чем сuseState.
  • Тестирование:Редьюсер — это чистая функция, не зависящая от вашего компонента. Это означает, что её можно экспортировать и тестировать отдельно в изоляции. Хотя обычно лучше тестировать компоненты в более реалистичной среде, для сложной логики обновления состояния может быть полезно убедиться, что ваш редьюсер возвращает определённое состояние для заданного начального состояния и действия.
  • Личные предпочтения:Кому-то нравятся редьюсеры, кому-то нет. Это нормально. Это вопрос предпочтений. Вы всегда можете конвертировать код междуuseState и useReducerтуда и обратно: они эквивалентны!

Мы рекомендуем использовать редьюсер, если вы часто сталкиваетесь с ошибками из-за некорректных обновлений состояния в каком-либо компоненте и хотите привнести больше структуры в его код. Не обязательно использовать редьюсеры для всего: не стесняйтесь смешивать и сочетать! Вы даже можете использоватьuseState и useReducerв одном компоненте.

Как правильно писать редьюсеры

При написании редьюсеров помните о двух советах:

  • Редьюсеры должны быть чистыми.Подобнофункциям обновления состояния, редьюсеры выполняются во время рендеринга! (Действия ставятся в очередь до следующего рендера.) Это означает, что редьюсерыдолжны быть чистыми—одинаковые входные данные всегда приводят к одинаковому результату. Они не должны отправлять запросы, планировать таймауты или выполнять любые побочные эффекты (операции, влияющие на что-то вне компонента). Они должны обновлятьобъекты и массивыбез мутаций.
  • Каждое действие описывает одно взаимодействие пользователя, даже если оно приводит к нескольким изменениям в данных.Например, если пользователь нажимает «Сбросить» в форме с пятью полями, управляемыми редьюсером, логичнее отправить одно действиеreset_form, а не пять отдельных действийset_field. Если вы логируете каждое действие в редьюсере, этот журнал должен быть достаточно понятным, чтобы восстановить, какие взаимодействия или ответы произошли и в каком порядке. Это помогает при отладке!

Написание лаконичных редьюсеров с Immer

Так же, как и приобновлении объектов и массивовв обычном состоянии, вы можете использовать библиотеку Immer, чтобы сделать редьюсеры более лаконичными. ЗдесьuseImmerReducerпозволяет вам изменять состояние с помощью присваиванияpushилиarr[i] =:

Редьюсеры должны быть чистыми, поэтому они не должны изменять состояние. Но Immer предоставляет вам специальный объектdraft, который безопасно изменять. Под капотом Immer создаст копию вашего состояния с изменениями, внесенными вdraft. Вот почему редьюсеры, управляемыеuseImmerReducer, могут изменять свой первый аргумент и не обязаны возвращать состояние.

Итоги

  • Чтобы перейти отuseState к useReducer:
    1. Отправляйте действия из обработчиков событий.
    2. Напишите функцию-редьюсер, которая возвращает следующее состояние для заданного состояния и действия.
    3. ЗаменитеuseStateнаuseReducer.
  • Редьюсеры требуют написания немного большего количества кода, но они помогают при отладке и тестировании.
  • Редьюсеры должны быть чистыми.
  • Каждое действие описывает одно взаимодействие пользователя.
  • Используйте Immer, если хотите писать редьюсеры в мутирующем стиле.

Try out some challenges

Challenge 1 of 4:Dispatch actions from event handlers #

Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.

Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.