Работа с динамическими данными и со стейтом - это одна из основных задач разработчика. Если логика изменения данных написана правильно, то и их отображение будет несложной задачей.
Первое приложение у нас выглядит следующим образом:
Все данные хранились в одном компоненте
Все данные передавались по иерархии вниз, а изменения состояния передавались вверх через коллбэки
Так же все состояния централизованы (они все находились в одном месте - в компоненте App)
Такой подход называется Property Drill, когда мы просверливаем пути для передачи состояний по уровням через несколько компонентов. Такой подход не является достаточно логичным, так как некоторые компоненты могут хранить в себе ненужные для них состояния, которые мы просто перебрасываем дальше.
Второе приложение выглядит уже следующим образом:
Каждый компонент хранит своё состояние у себя (один компонент содержит список персонажей, а другой список комиксов, третий содержит информацию об одном конкретном персонаже и так далее)
Такой подход сложно масштабировать, особенно, если появятся зависимости между компонентами
Чтобы решить вышеописанные проблемы, были придуманы определённые паттерны для работы с состояниями продуктов, такие как MVC, MVP, MVVM
И чтобы решить проблему со сложными зависимостями, можно создать один большой источник стейтов для всех компонентов. Однако тут мы сталкиваемся с проблемой, что каждый компонент может поменять наш глобальный стейт
И чтобы решить уже вышеописанную проблему, был придуман следующий подход:
Мы имеем наши компоненты View, которые при выполнении какого-либо действия создают Actions (который уже знает, что нужно обновить в стейте)
Определённые события Actions (которые хранят информацию о требуемых изменениях) вызывают срабатывание определённых действий в компоненте Reducer (который уже знает, как именно обновить этот стейт). Операция передачи объекта Actions в Reducer называется dispatch
Компонент Reducer - это компонент, который находится в общем хранилище стейтов и он знает, что делать при любом запросе от компонентов сайта. То есть он регулирует обновление стейтов внутри S, чтобы компоненты могли перерисоваться на базе обновлённых данных
Компонент S так же находится внутри хранилища и сам по себе просто хранит все состояния приложения.
Так же в Redux имеются селекторы - это функции, которые получают часть данных из хранилища для дальнейшего использования (из S во View)
И вот так выглядит работа стейт-менеджера на реальном примере. Из State прошлые данные приходят в Reducer, чтобы сравнить с новыми значениями.
Примерно такой же подход использовался в хуке useReducer.
И тут важно уточнить, что запутаться в трёх разных документациях легко, поэтому нужно знать. что ищем:
Так же очень важное расширение для работы с редаксом в браузере, которое позволяет просмотреть состояния системы:
002 Основные принципы Redux. Практика
Первым делом нужно установить библиотеки редакса в приложение
Дальше распишем базовую схему, которая будет соответствовать архитектуре работы редакса:
начальное состояние
функция-редьюсер
стейт
Уже таким образом функция редьюсера будет написана лаконичнее, так как действий может быть множество
И тут нужно сказать, что стоит установить дефолтное значение, так как в функцию может попасть undefined, что может привести к ошибке
Ну и далее создадим единый стор, который уже принимает в себя функцию-редьюсер. Обычно в приложении располагается только один стор
Так же мы можем реализовать подписку на изменения в сторе, что позволит контролировать изменение состояний в приложении
Важные правила работы с Reducer:
Эта функция должна быть чистой и зависеть только от приходящего в неё стейта и экшена
Она должна возвращать один и тот же результат при одинаковых аргументах и не иметь никаких побочных эффектов (никаких логов, запросов на сервер, генераций случайных чисел и никакой работы с ДОМ-деревом)
Вёрстка кнопок
Стили
И использование редакса на странице:
Отдельно нужно сказать, что так делать нельзя и выше было показано, что мы передали это значение через свойство payload (полезная нагрузка)
И так же в Redux используется actionCreator функция, которая генерирует экшены. Они используются для более безопасного применения редьюсера, чтобы он возвращает не стейт по дефолтному проходу, а ошибку, если мы передали неправильный объект
Но так же мы будем часто работать с данными в виде объекта, поэтому и писать придётся код соблюдая иммутабельность:
переводим начальный стейт в объект
меняем редьюсер на работу со стейтом по принципу иммутабельности (разворачиваем старый объект и добавляем новые данные)
далее из стора нужно будет получить не целый объект, а одно значение store.getState().value
Итог: мы имеем каунтер построенный на базе отслеживания состояния через редакс даже без использования React на чистом JS
003 Чистые функции
Понятие чистой функции исходит из обычного программирования и там это имеется ввиду, когда говорят про прозрачность работы функции
Особенности чистых функций:
При одинаковых данных они всегда возвращают одинаковый результат
Она не вызывает внутри себя побочных эффектов
Представленная функция всегда будет возвращать разные значения ровно по той причине, что она всегда выполняет в себе побочное действие (генерирует рандомное число)
И теперь, когда мы переделали функцию таким образом, она является чистой ровно потому, что при передаче одних и тех же аргументов она всегда будет возвращать тот же результат
Так же тут нужно понимать, что все зависимости должны находиться внутри данной функции - значений извне она принимать не может
Побочные действия, которые нельзя использовать в чистых функциях:
Все асинхронные операции (запросы на сервер, изменение файлов)
Получение рандомного значения
Вывод логов
Работа с ДОМ-деревом
Видоизменение входных данных (это нарушение иммутабельности)
004 Оптимизация через actionCreators и bindActionCreator
Далее попробуем разбить приложение на отдельные файлы
Экшены вынесем в отдельный файл
actions.js
Сам редьюсер уберём в другой файл
reducer.js
И далее основную логику приложения оптимизируем:
деструктуризируем и достанем из стора повторяющиеся функции dispatch, subscribe и getState
Ивентлистенеры повторяют одну и ту же вложенную функцию - вызывают экшен-функцию внутри dispatch. Это поведение можно оптимизировать и вынести в отдельную функцию-диспэтчер (incDispatch и так далее)
index.js
Однако очень часто разработчики для простоты использования кода создавали функцию bindActionCreator, которая возвращала уже сбинженную функцию диспэтча для вызова в других местах
index.js
Однако в редаксе уже есть подобная функция bindActionCreators, которая за нас создаёт подобный связыватель
index.js
Так же мы можем сделать привязку нескольких функций через одну функцию bindActionCreators, но уже через объект
index.js
Можно ещё сильнее сократить запись, если импортировать не все именованные импорты по отдельности, а импортировать целый объект и его вложить первым аргументом
index.js
005 Добавим React в проект
Сначала выделим компонент счётчика в отдельный реакт-компонент
Counter.js
Далее передадим все функции, которые нужны для работы компонента и обернём рендер реакт-компонента в функцию update, которая будет вызваться через subscribe, когда у нас обновится значение в редаксе
Тут нужно отметить, что такой подход не используется в реальных проектах
index.js
И счётчик работает
И сейчас подготовим проект для того, чтобы он мог работать вместе с редаксом:
Первым делом нужно убрать все импорты и экспорты разных функций и экшенов. Единственное, что нам нужно - это создать глобальное хранилище createStore и закинуть в него reducer. Далее нам нужно вложить все компоненты приложения в Provider, который отслеживает все изменения стора и распространяет данные по приложению. В провайдер нужно передать будет и сам store
Тут нужно упомянуть, что провайдер сам отслеживает изменения и сам сигнализирует компонентам, что данные были изменены
index.js
И далее нужно просто вызвать компонент счётчика внутри App
components > App.js
Далее, чтобы приложение заработало, нужно будет с помощью connect распространить данные по всем компонентам приложения. Это позволит прокинуть в Counter нужные функции и данные для работы со счётчиком. Пока же компонент не работает без данных манипуляций.
006 Соединяем React и Redux при помощи connect
Подключить Redux к React можно двумя способами:
функция connect, которая используется в классовых компонентах и в старых проектах
более корректен в плане написания кода, но менее производительный
Далее нужно реализовать контроль состояния в нашем каунтере
Для начала, можно перенести логику по генерации рандомного значения прямо в actionCreator-функцию
actions.js
Первым делом, нужно обернуть вывод компонента в функцию connect, получаемую из реакт-редакса. Передаётся функция во вторые скобочки (аргументы вложенного ретёрна). В первые скобки уже будут приниматься аргументы самого коннекта
Работает connect по следующей цепочке:
внутри приложения какой-либо компонент задиспетчил (изменил стейт) какое-либо действие
глобальное состояние изменилось
провайдер отлавливает изменение и даёт сигнал всем компонентам, которые находятся внутри
дальше запускается connect от провайдера
запускается функция mapStateToProps
и если пропсы компонента поменялись, то весь компонент будет перерисован
И далее создадим две функции mapStateToProps и mapDispatchToProps, чтобы получить:
из первой функции значение из стора
с помощью второй функции сгенерировать три функции-диспэтча
Counter.js
Функция коннекта принимает в себя 4 необязательных значения:
mapStateToProps в виде функции, которая запросит данные из стейта
mapDispatchToProps в виде функции, которая сгенерирует объект с диспетчами или в виде объекта (коннект сам распарсит объект и сделает из него нужные функции)
mergeProps и options используются для оптимизации работы функции connect
Функция mapStateToProps применяется для получения данных из стейта и используется внутри коннектора. Она должна быть чистой и синхронной, как функция-редьюсер
То есть функция будет получать данное начальное установленное значение
Функция mapDispatchToProps уже имеет предназначение формировать в себе нужные диспэтчи под определённые компоненты
Тут так же нужно сказать, что у нас есть 4 варианта реализации данной функции в зависимости от степени абстракции (первые три функции реализованы с учётом неизменённого actionCreatorа):
Однако дальше нужно сказать, что вторым аргументом connect может получить не только функцию, где мы сами разбиваем actionCreator'ы, а просто передать объект, который уже функция коннекта сама разберёт
Однако такой подход работает только тогда, когда нам не нужно проводить дополнительные манипуляции над actionCreatorами
Counter.js
Итог: каунтер наконец-то работает
Ну и так выглядит функция с использованием классового компонента:
007 Соединяем React и Redux при помощи хуков
Так же куда более простым способом в реализации подключения редакса к реакту будет использование хуков:
useSelector - позволяет получить из глобального хранилища (стора) нужное нам состояние
useDispatch - предоставляет доступ к функции dispatch
Counter.js
Отличия useSelector от mapStateToProps:
хук возвращает всё, что угодно, а не только то, что идёт на пропсы
коллюэк функция позволяет сделать всё, что угодно с данными, но она должна оставаться чистой и синхронной
в само значение, которое вызывает функцию, может помещаться что угодно (строка, массив, функция и так далее)
в хуке отсутствует свойство ownProp, которое используется для передачи собственных пропсов для отслеживания
при срабатывании диспэтч-функции, хук сам проверяет не изменились ли данные, которые он возвращает. Тут уже проверка проходит не по всему объекту, как в обычной функции, а по отдельным полям объекта (если мы сразу возвращаем объект, но если мы возвращаем через return, то тут уже будет проходить проверка по всему объекту)
Так же хук при изменении стейта в сторе будет вызывать перерендер компонента
Так же, когда мы возвращаем из функции новый объект, то у нас каждый раз будет создаваться новый объект, что будет вызывать перерендеры компонента. Чтобы избавиться от данной ошибки, можно:
просто дублировать использование хука useSelector при запросе отдельных свойств из стора
использовать функцию Reselect из сторонней библиотеки
либо можно использовать функцию shallowEqual:
Если мы говорим про хук useDispatch, то тут нужно упомянуть, что при передаче его дальше по иерархии в нижние компоненты, нужно обернуть его в useCallback, чтобы каждый раз не пересоздавался диспэтч. Дело в том, что пересоздание диспэтча будет вызывать пересоздание и самого компонента.
Так же существует хук useStore, который возвращает полностью весь объект стора, но им пользоваться не стоит
В конце стоит отметить, что показанный в начале пример использования компонента с хуками - стоит использовать как конечный вариант. Не стоит использовать оборачивать хуки редакса в дополнительные хуки.
Zombie Childrens
zombie children: дочерние компоненты, о которых родитель ничего не знает
stale props: протухшие свойства - свойства, которые не являются актуальными в данный конкретный момент времени
Большинство разработчиков даже не представляют себе что это такое и когда это может возникнуть.
zombie children — давняя проблема попытки синхронизировать внешнее синхронное хранилище состояния (react-redux) с асинхронным циклом рендеринга React.
Проблема кроется в порядке возникновения события ComponentDidMount/useEffect у компонентов React при их монтировании в дерево компонент в иерархиях родитель-дети, в ситуации, когда эта связка компонент отображает структуры данных типа “список” или “дерево” и эти компоненты подписаны на изменения в источнике данных, который находится вне контекста React.
Для начала давайте рассмотрим типичный PubSub объект
Глядя на listeners мы должны понимать одно: так как элементы массива хранятся в том порядке в котором они добавлялись — коллбэки подписчиков вызываются ровно в том порядке, в котором происходили подписки.
Теперь давайте посмотрим в каком порядке происходит монтирование компонент в иерархии компонент родитель-дети:
Если каждому компоненту в ComponentDidMount добавить запись в консоль имени монтируемого компонента, то мы увидим следующее:
mounting component B
mounting component C
mounting component A
Обратите внимание: родительский компонент А монтируется после своих детей (его метод ComponentDidMount вызывается последним)!
Рассмотрим использования redux контейнеров:
Если в каждом из компонентов, в методе ComponentDidMount происходит подписка subscribe на оповещение об изменении данных, то при возникновении изменений в источнике данных сначала будут вызваны коллбеки у дочерних компонент и лишь затем — у компонента-родителя.
Теперь представим, что в источнике данных мы удалили данные:
1: { id: 1, title: 'Component1', text: '...' },
Первым будет вызван коллбэк для компонента ListItemContainer с id=1 (так как он до изменения данных первым монтировался и первым подписался), компонент пойдет в источник данных за данными для отрисовки, а данных там для него уже нет!
Попытка получения данных, путем обращения
const { someProp } = store.list[1];
приведет к краху приложения с ошибкой типа “Uncaught TypeError: Cannot read property ‘1’ of undefined.” или подобной (ошибки может и не быть если сначала проверить на существование элемент в сторе, но компонент в дереве присутствует и он — зомби);
Оказывается, что компонент с id=1 в данный момент не является дочерним для компонента-контейнера ListContainer, хотя на момент возникновения изменений в источнике данных он находится в дереве DOM— зомби-ребенок
В некоторых ситуация эти брошенные дочерние компоненты могут остаться в дереве даже после перерисовки родителя.
С zombie children разобрались. Теперь пора выяснить что такое stale props.
Рассматривая последний пример: давайте представим что для элемента с id=1 мы в источнике данных поменяли title. Что произойдет?
Сработает коллбэк для компонента ListContainer с id=1, он начнет перерисовку и отобразит title, который был ему передан в свойствах компонентом ListContainer, до изменений в данных — title в данном случае является stale props!
Почему же многие разработчики этого не знают? Потому что эти проблемы от них тщательно скрывают!! 😊
К примеру, разработчики react-redux поступают следующим образом — оборачивают отрисовку дочерних компонент в try…catch, при возникновении ошибки — они устанавливают счетчик ошибок в 1 и вызывают перерисовку родителя. Если в результате перерисовки родителя и последующей перерисовке дочерних компонент снова возникает ошибка и счетчик > 0 — значит это не zombie children, а что-то более серьезное, поэтому они прокидывают эту ошибку наружу. Если ошибка не повторилась — это был зомби-ребенок и после перерисовки родителя он пропадет.
Есть и другой вариант — изменяют порядок подписки так, чтобы родитель всегда подписывался на изменения раньше чем дочерние компоненты.
Но, к сожалению, даже такие попытки не всегда спасают — в react-redux предупреждают, что при использовании их хуков все же могут возникать указанные проблемы с zombie children & stale props, т.к. у них происходит подписка на события стора в хуке useEffect (что равнозначно componentDidMount), но в отличие от HOCа connect - не кому исправлять порядок подписки и обрабатывать ошибки.
Не полагаться на свойства компонента в селекторе при получении данных из источника
В случае если без использования свойств компонента не возможно выбрать данные из источника — пытайтесь выбирать данные безопасно: вместо state.todos[props.id].name используйте todo = state.todos[props.id для начала и затем после проверки на существование todo используйте todo.name
чтобы избежать появления stale props — передавайте в дочерние контейнеры только ключевые свойства, по которым осуществляется выборка всех остальных свойств компонента из источника — все свойства всегда будут свежими
При разработке своих библиотек и компонент React, разработчику всегда нужно помнить об обратном порядке генерации события жизненного цикла ComponentDidMount родителя и детей в случае использования подписок на события одного источника данных, когда данные хранятся вне контекста React, чтобы не возникали ошибки данного рода.
Но лучший совет — хранить данные внутри контекста исполнения React в хуке useState или в Context /useContext— вы никогда не столкнетесь с вышеописанными проблемами т.к. в функциональных компонентах вызов этих хуков происходит в естественном порядке — сначала у родителя, а затем — у детей.
008 Redux devtools
Когда мы работаем со старым АПИ редакса, нужно использовать вторым аргументом данную строку, чтобы подключить тулзы разработчика (если пишем на современном, то обойтись можно и без этого)
И примерно таким образом выглядит интерфейс тулза:
Так же мы можем просмотреть список переходов изменений разных стейтов в виде графа
И самая частоиспользуемая вкладка просмотра разницы между состояниями
Ну и так же отображается список выполненных экшенов
Так же присутствует таймлайн для отмотки состояний в приложении
009 Правило названия action и домашнее задание (мини-экзамен)
Структура проекта выглядит примерно следующим образом:
Это тот файл приложения, который будет шэриться через json-server и от которого будут выводиться новые посты
heroses.json
Стор редакса
src > store > index.js
Редьюсер редакса. Пока он один, но в дальнейшем будет пополняться их количество.
Все типы экшенов должны быть написаны заглавными буквами. Если они относятся к запросам на сервер, то мы имеем состояние отправки запроса на сервер, полученного ответа от сервера или ошибки.
Второй кейс редьюсера так же в качестве payload принимает в себя список героев, который будет отображаться на странице
src > reducer > index.js
А уже тут описаны экшены редакса.
Экшен heroesFetched принимает в себя так же список героев, который пришёл от сервера и сохраняет его в состояние.
src > actions > index.js
Хук отправки запроса на сервер будет возвращать один ответ от сервера
src > hooks > http.hook.js
Тут уже располагается вся основная часть приложения
src > index.js
Это основной компонент App
src > components > app > App.js
Чтобы запустить два сервера вместе (react и json-server), нужно будет установить дополнительную библиотеку, которая позволяет запустить две команды одновременно:
И так теперь выглядит сдвоенный запрос:
package.json
Это компонент, который выводит список элементов карточек героев
components > heroesList > HeroesList.js
А это компонент самой карточки
components > heroesListItem > HeroesListItem.js
Тут уже находится вёрстка компонента смена активностей классов:
components > heroesFilters > HeroesFilters.js
Тут представлена вёрстка формы для добавления персонажей без логики
components > heroesAddForm > HeroesAddForm.js
И так выглядит итоговое приложение, которое нужно дорабатывать, чтобы оно отправляло запросы на json-server, создавало новых персонажей, меняло стейт и фильтровало персонажей по элементам:
010 Разбор самых сложных моментов
Фильтры были расширены и внутрь них были помещены дополнительные данные по лейблу и классам, которые нужно будет вставить в кнопки
heroes.json
В экшены были добавлены креэйторы, которые отвечают за состояние фильтров и состояние добавления персонажей
actions > index.js
В редьюсер были добавлены кейсы для добавления персонажа, удаления и реагирование на изменение фильтра. Так же было добавлены дополнительные состояния в хранилище
reducers > index.js
В компонент списка героев добавилась функция, которая позволяет удалить персонажа и она передаётся в компонент с одним персонажем. Так же была добавлена анимация для удаления и появления элементов в списке
components > heroesList > HeroesList.js
В компонент одного героя была добавлена только функция для удаления персонажа, которая приходит из списка
components > heroesListItem > HeroesListItem.js
В форму добавления нового персонажа были добавлены состояния для контроля инпутов.
Была добавлена функция onSubmitHandler, которая контролирует действие при отправке формы
components > heroesAddForm > HeroesAddForm.js
Тут были добавлены фильтры, которые мы получаем с сервера
components > heroesFilters > HeroesFilters.js
Теперь работает фильтрация и добавление персонажей
011 Комбинирование reducers и красивые селекторы. CreateSelector()
При разрастании приложения увеличивается и количество действий, которые должен контролировать реакт. Если экшены можно спокойно разделить по папкам и обращаться конкретно к нужным, то данное разрастание не позволит спокойно поделить функцию-редьюсер
В нашем приложении достаточно логичным будет отделить логику работы с персонажами и фильтрами. Однако мы сталкиваемся с тем, что фильтры так же используют состояние персонажей, чтобы контролировать их список.
Чтобы сократить код и разбить логику, можно:
разделить логику редьюсера через функцию combineReducers
вынести фильтрацию внутрь компонента, чтобы разбить логику стейтов
Тут мы выносим фильтрацию полученных данных из стейта и теперь её не нужно проводить внутри редьюсера
components > heroesList > HeroesList.js
Теперь нам не нужно данное состояние
И данная фильтрация в редьюсере
Вынесем из главного reducer логику по работе с персонажами и его стейты в отдельный файл
reducers > heroes.js
Тут уже будем хранить логику фильтрации
reducers > filters.js
И тут через функцию combineReducers объединяем две функции редьюсера в один внутри объекта. Теперь обычный reducer не нужен и его можно будет удалить
store > index.js
И теперь, после манипуляций с объединением редьюсеров, нужно будет вытаскивать нужные объекты из объектов, которые были названы и переданы в combineReducers
И вот уже с таким синтаксисом мы можем импортировать поля из нескольких объектов
Однако такой подход приводит к тому, что компонент будет перерисовываться при каждом изменении стейта
Такой вариант не стоит использовать в проекте, так как он не оптимизирован
Откорректируем логику фильтрации героев.
Но тут мы встретимся с такой проблемой, что каждый раз при нажатии кнопки фильтрации, у нас будет воспроизводиться перерендер компонента. Это происходит из-за того, что каждый раз у нас вызывается useSelector() при изменении глобального стейта.
components > heroesList > HeroesList.js
Чтобы решить данную проблему, нужно мемоизировать функцию вызова useSelector()
Данный модуль позволяет нам вызвать по определённым правилам функцию useSelector. То есть мы создаём массив запросов в селектор первым аргументом, а вторым аргументом берём полученные значения и используем их в функции, которую хотели использовать в селекторе.
После вышеописанных манипуляций просто помещаем функцию реселекта внутрь useSelector
components > heroesList > HeroesList.js
Теперь рендер вызвается только тогда, когда данные в стейте изменяются
012 Про сложность реальной разработки
Реальные приложения требуют от разработчика большое количество знаний - это сложно, но нужна практика
Спасибо за внимание
013 Store enhancers
Store enhancers - это дополнительный функционал, который упрощает взаимодействие с хранилищем. Зачастую просто используют сторонние npm-пакеты, но так же можно написать и свой функционал улучшителя.
Так же частным случаем энхэнсеров является middleware функции, которые так же передаются в стор.
Конкретно для нашего проекта можно сделать простой энхэнсер, который модифицирует работу диспэтча. Он будет в себя принимать не только объект с определённым действием, но и принимать строку с экшен тайптом.
Тут уже нужно сказать, что самих улучшителей стора может быть большое количество и поэтому их часто передают внутри функции compose, которая объединяет их в один. Однако так же нужно будет соблюдать последовательно передачи функций, так как они будут модифицировать логику последовательно. Конкретно в данном случае, строку с подключением к редакс-девтулзу стоит поместить в конец списка.
Приложение так же работает, но теперь у нас есть возможность передавать в диспетч и просто строку с действием
014 Middleware
Middleware - это enhancer, который занимается улучшением только dispatch. Так же зачастую пользуются уже готовыми middleware, которые предоставляет комьюнити npm
Конкретно тут сделаем посредника, который позволит dispatch принимать не только объекты, но и строки
store > index.js
первым аргументом можно так же ничего не передавать, потому что нам не всегда нужен store
обычно, функцию dispatch называют next, так как будет вызываться следующая функция из middleware
store > index.js
Чтобы применять middleware в createStore, нужно будет воспользоваться функцией applyMiddleware, которая будет применять посредника.
Чтобы вернуть подключение к редакс-девтулзу, можно опять же обернуть весь второй аргумент createStore в функцию compose()
store > index.js
015 Redux-thunk
Основная задача модуля redux-thunk передавать функцию, которая потом будет производить асинхронную операцию
Устанавливаем пакет в проект.
И далее, чтобы убедиться, что он работает, можно просто попробовать передать actionCreater функцию в dispatch без вызова:
components > heroesList > HeroesList.js
Так же мы можем расширять наши экшены, так как в их вложенную функцию может автоматически поступать dispatch, над которым мы можем проводить различные манипуляции.
Конкретно тут будет срабатывать передача данных в стейт через определённый промежуток времени.
actions > index.js
Но так же мы можем и упростить себе жизнь тем, что мы можем вызвать логику диспетча прямо внутри самой папки экшенов.
Конкретно, мы можем вынести запрос на получение персонажей и занесение их в стейт прямо из экшенов. Там нам не нужно будет импортировать и экспортировать отдельные экшены - можно будет ими просто воспользоваться.
actions > index.js
И тут далее в самом компоненте уже можем воспользоваться одним экшеном, который сам занесёт данные по персонажам в стейт, передав в него функцию совершения реквеста
очень много boilerplates при создании actionCreators и reducers
при большом количестве enhancers и middlewares функция по созданию store сильно разрастается
Redux Toolkit включает в себя набор инструментов для более простой и быстрой работы с states и store.
Та же функция createSelector была переэкспортирована из модуля Reselect в RTK
Redux Toolkit configureStore()
Функция configureStore предназначена для того, чтобы удобно автоматически регулировать reducers, подключать middlewares или enhancers и автоматически подключать redux devtools без дополнительных строк кода
В тулкит так же включены изначально самые популярные middlewares:
Serializability Middlweware - проверяет, чтобы в стейте были только те значения, которые должны быть в сторе
Immutability Middlweware - предназначен для обнаружения мутаций, которые могут быть в сторе
Thunk Middlweware - позволяет в экшены автоматически получать dispatch
И уже так будет выглядеть создание нового store с использованием RTK
store > index.js
Redux Toolkit createAction()
Функция createAction() позволяет автоматически выполнять операцию по созданию экшена
Функция принимает в себя:
тип действия
вспомогательную функцию
И далее тут нужно сказать, что данная функция автоматически обрабатывает поступающие в неё данные. Т.е. если вызвать heroesFetched и передать в него аргумент, то он автоматически отправится в поле payload
Ниже представлены две реализации экшенов - классическая и через createAction и обе из них работают полностью взаимозаменяемо
Тут уже стоит отметить, что в reducer стоит передавать только одно поле payload. Таким образом будет проще читать код и воспринимать его. Остальные побочные действия лучше делать вне reducer.
И вот пример, когда мы вторым аргументом передаём дополнительную функцию, которая осуществляет возврат обогащённого payload, который уже будет содержать не просто переданные данные, а ещё и сгенерированные нами
Тут так же стоит отметить, что в RTK была добавлена функция nanoid, которая генерирует уникальный идентификатор для объекта
Redux Toolkit createReducer()
Функция reducer зачастую представляет из себя очень много блоков switch-case и много вложенных конструкций, которые нужно редактировать в глубине, что усложняет разработку
Для упрощения создания reducer была добавлена функция createReducer, которая принимает в себя:
начальное состояние
builder, который позволяет строить reducer за счёт встроенных в него трёх функций
builder использует три функции:
addCase - добавляет кейс в свитчер редьюсера
addDefaultCase - устанавливает дефолтный кейс выполнения
addMatcher - позволяет фильтровать входящие экшены
И так выглядит реализация нашего редьюсера героев через createReducer:
reducers > heroes.js
Так же нужно отметить, что внутри функций builder используется библиотека ImmerJS, которая сама отвечает за сохранение логики иммутабельности в проекте. То есть мы можем писать визуально проект с мутациями, а библиотека сама переведёт код в иммутабельные сущности.
Такой подход будет работать ровно до тех пор, пока мы ничего не возвращаем из этих функций через return
Однако функция createReducer требует для работы, чтобы все экшены были написаны с помощью createAction
actions > index.js
Так же у нас есть вариант использовать более короткий способ создания редьюсеров через объект. Такой способ уже не работает с TS.
reducers > heroes.js
Redux Toolkit createSlice()
Данная функция объединяет функции createAction и createReducer в одно
Обычно она располагается рядом с файлом, к которому она и относится
В конец названия файла обычно добавляется суффикс Slice
Функция createSlice принимает в себя 4 аргумента:
name - пространство имён создаваемых действий (имя среза). Это имя будет являться префиксом для всех имён экшенов, которые мы будем передавать в качестве ключа внутри объекта reducers
initialState - начальное состояние
reducers - объект с обработчиками
extraReducers - объект с редьюсерами другого среза (обычно используется для обновления объекта, относящегося к другому слайсу)
Конкретно тут был создан срез actionCreators и reducer для героев в одном файле рядом с самим компонентом
components > heroesList > HeroesList.js
И далее импортируем наш reducer в store
store > index.js
Теперь всё то, что относится к actionCreators героев можно удалить из файла экшенов и импортировать нужные зависимости для работы функции fetchHeroes
actions > index.js
Далее нужно поправить некоторые импорты в HeroesList и в HeroesAddForm
И теперь мы имеем работающее приложение, которое мы переписали на более коротком синтаксисе.
Однако тут стоит сказать, что теперь наши действия были переименованы под образ createSlice, где обозначается пространство выполняемых действий экшеном (heroes) и сам actionCreator (heroesFetching)
Если нам нужно будет не только получить, но и обогатить payload, то можно будет добавить передать в экшен два объекта:
reducer - это сам обработчик
prepare - обработчик, который обогащает payload
Если нам нужно изменить стейт уже в другом компоненте из нашего, то мы можем воспользоваться для этого extraReducers
Redux Toolkit createAsyncThunk()
Функция createAsyncThunk() позволяет сделать асинхронный actionCreator, который будет вести себя ровно так же, как и при использовании обычного redux-thunk.
Использование данной функции является приоритетным, так как при таком запросе heroes/fetchHeroes функция возвращает нам три экшена, которые поделены на:
pending: 'heroes/fetchHeroes/pending'
fulfilled: 'heroes/fetchHeroes/fulfilled'
rejected: 'heroes/fetchHeroes/rejected'
Такой подход позволит нам не обрабатывать три разных состояния функции самостоятельно, а перекладывать это на функционал тулкита.
Тут нужно отметить, что из данной функции мы должны возвращать Promise, который функция сама и обработает по трём состояниям
Сам reducer, который мы создали через createAsyncThunk будет передаваться в основной reducer уже как четвёртый аргумент - объект extraReducers
Тут мы создали функцию fetchHeroes, которая заменит fetchHeroes находящийся в actions. Далее нужно будет обработать три состояния fetchHeroes уже внутри самого heroesSlice, передав внутрь extraReducers
components > heroesList > heroesSlice.js
Теперь тут меняем импорты
components > heroesList > HeroesList.js
Ну и так же из нашего хука useHttp нужно убрать useCallback, так как это приведёт к ошибке
И теперь всё работает и функция за нас реализовала сразу три состояния стейта
Redux Toolkit createEntityAdapter()
Функция createEntityAdapter() позволит создавать готовый объект с часто-выполняемыми CRUD-операциями в reducer
В самом начале в файле со слайсом нужно создать сам адаптер и переписать создание initialState под адаптер
Так же мы можем внутрь адаптера вложить свойства, которые мы не хотим обрабатывать через него (heroesLoadingStatus), а хотим обработать самостоятельно
components > heroesList > heroesSlice.js
Если вывести адаптер в консоль, то он будет иметь в себе объект, который будет хранить все попадающие внутрь него сущности и идентификаторы. Так же он будет отображать все те поля, которые мы передали как объект внутрь адаптера - уже с ними можно будет работать отдельно без круд-функций адаптера
Так же нужно сказать, что функция createEntityAdapter принимает в себя объект с переопределением начальных функций
CRUD-операции, которые предоставляет createEntityAdapter:
addOne: принимает один объект и добавляет его, если он еще не присутствует.
addMany: принимает массив сущностей или объект в форме Record<EntityId, T> и добавляет их, если они еще не присутствуют.
setOne: принимает отдельный объект и добавляет или заменяет его
setMany: принимает массив сущностей или объект в форме Record<EntityId, T> и добавляет или заменяет их.
setAll: принимает массив сущностей или объект в форме Record<EntityId, T> и заменяет все существующие сущности значениями в массиве.
removeOne: принимает единственное значение идентификатора объекта и удаляет объект с этим идентификатором, если он существует.
removeMany: принимает массив значений идентификатора объекта и удаляет каждый объект с этими идентификаторами, если они существуют.
removeAll: удаляет все объекты из объекта состояния сущности.
updateOne: принимает “объект обновления”, содержащий идентификатор объекта, и объект, содержащий одно или несколько новых значений поля для обновления внутри changes поля, и выполняет поверхностное обновление соответствующего объекта.
updateMany: принимает массив объектов обновления и выполняет мелкие обновления для всех соответствующих объектов.
upsertOne: принимает единую сущность. Если объект с таким идентификатором существует, он выполнит поверхностное обновление, и указанные поля будут объединены в существующий объект, а любые совпадающие поля будут перезаписывать существующие значения. Если объект не существует, он будет добавлен.
upsertMany: принимает массив объектов или объект в формеRecord<EntityId, T>, который будет слегка изменен.
Все вышеописанные методы следуют принципу нормализации данных. Они производят действия над данными по определённым условиям, если они существуют/не существуют
Реализуем добавление персонажей в массив через адаптер. Для этого нам может подойти функция setAll, в которая будет являться местом, куда помещаем все данные, а вторым аргументом данные для помещения.
components > heroesList > heroesSlice.js
Все данные, которые мы помещаем в стейт, отправляются в объект entities
Чтобы работать с данным объектом и получать из него нужные сущности, нужно воспользоваться функциями выбора. Адаптер выбранной сущности содержит метод getSelectors(), которая предоставляет функционал селекторов уже знающих как считывать содержимое этой сущности:
selectIds: возвращает массив с идентификаторами state.ids.
selectEntities: возвращает объект state.entities.
selectAll: возвращает массив объектов с идентификаторами state.ids.
selectTotal: возвращает общее количество объектов, сохраняемых в этом состоянии.
selectById: учитывая состояние и идентификатор объекта, возвращает объект с этим идентификатором или undefined.
Если мы использем селекторы в глобальной областивидимости, то нам нужно будет самостоятельно указывать, с чем именно должна работать данная команда
И теперь нам нужно добавить функционал по вытаскиванию всех элементов из стейта. Сделать это легко - мы просто из файла со слайсом будем экспортировать функцию selectAll, которую привяжем к state.heroes
components > heroesList > heroesSlice.js
Вторым аргументом в листе мы возвращали с помощью отдельной функции список всех персонажей. Теперь же можно вернуть всё с помощью функции-селектора
И теперь приложение работает, так как на фронт попадает тот массив, который нам и был нужен
Если мы попытаемся вывести массив с логами о героях, то тут можно увидеть, что в первые две смены состояния были пустые, но дальше мы получили массив с объектами
И теперь можно переписать все операции модификации стейта на круд-операции из самого адаптера.
Тут нужно сказать, что данные по reducer, действия над которыми происходят в пространстве имён heroes, будут помещаться в state.entities.heroes. Однако напрямую с ними взаимодействовать не придётся, так как мы их можем автоматически достать через селекторы
Ну и так же можно оптимизировать код и создавать селекторы (библиотека Reselect) уже внутри самого слайса
Вышеописанный подход с использованием Redux позволяет нам скрывать логическую часть работы с данными от самого компонента, который эти данные отображает. Теперь View работает отдельно и занимается только отображением данных без какого-либо их преобразования.
024 Redux Toolkit RTK Query
RTK Qeury и React Query концептуально меняют подход к использованию данных в приложении. Они предлагают не изменять глобальные состояния, а оперировать загруженными данными
Сейчас наше взаимодействие выглядит так:
мы отправляем запрос на сервер
мы получаем данные с сервера
отправляем изменение состояния в стейт
Далее потребуются две основные функции для работы с Query:
createApi - полностью описывает поведение RTK Query
fetchBaseQuery - модифицированная функция fetch()
Чтобы начать работать с данной библиотекой, нужно будет написать будущее АПИ общения с RTK Query:
Пишем функцию createApi, которая описывает взаимодействие с библиотекой и передаём в неё объект
reducerPath будет указывать то пространство имён, в котором происходят все запросы
baseQuery описывает полностью базовые параметры запроса на сервер
функция fetchBaseQuery выполняет функцию фетча, но хранит дополнительные параметры для ртк
baseUrl принимает строку для обращения к серверу
endpoints хранит функцию, которая возвращает объект с теми запросами и изменениями, что мы можем вызвать
свойство объекта будет входить в имя хука, который будет сгенерирован. Если мы имеем имя getHeroes, то библиотека сформирует хук useGetHeroes[Query/Mutation] (суффикс уже будет зависеть от типа того, что делает хук - просто запрос или мутация данных)
api > apiSlice.js
Далее нужно сконфигурировать хранилище:
чтобы добавить новый reduce, нужно в качестве свойства указать динамическую строку apiSlice.reducerPath и указать значение переменной самого редьюсера apiSlice.reducer
далее добавляем middleware для обработки специфических запросов RTK Query
store > index.js
И уже тут мы можем воспользоваться хуком, который сгенерировал Query. Через хук useGetHeroesQuery мы получаем все те промежуточные состояния, которые могут быть присвоены запросы, который приходит с сервера
Так же нужно упомянуть, что все те данные, что мы получили с сервера будут кешироваться в браузере на определённое время
components > heroesList > HeroesList.js
И наше приложение работает теперь так же, как и до изменений - список героев нормально получается с сервера
Далее добавим запрос на мутацию стейта, который будет отправлять на сервер запрос на добавление персонажа в список
api > apiSlice.js
И далее можно будет применить данный хук мутации в коде:
хук возвращает массив из двух объектов:
функция отправки мутации данных
объект со статусом обработки запроса (тот же объект, что и у query)
далее можно будет применить функцию отправки героя на сервер и передать в него нового героя
и для нормальной работы всех обработчиков (объект из второго аргумента) используется функция unwrap()
Однако после отправки запроса на сервер, мы не получаем на главной странице нового списка персонажей с нашим созданным героем.
Чтобы исправить данную ситуацию, нам нужно будет использовать наш стейт api и обновлять стейт на фронте, когда мы получаем актуальные данные с сервера
Чтобы подвязать выполнение одних запросов под другие, нужно использовать теги в createApi
И теперь тут правим ситуацию:
объявляем глобально в АПИ поле tagTypes, которое принимает в себя массив тегов, которые будут использоваться для общения между методами
добавляем в первый запрос providesTags и тег, по которому будет оповещаться данный метод, чтобы он сработал при изменении данных
добавляем в запрос мутации invalidatesTags, который будет отправлять в хранилище тегов запрос, откуда на все подписанные методы с подходящими тегами будет приходить уведомление о переиспользовании
api > apiSlice.js
И теперь всё работает - при создании нового персонажа триггерится функция обновления списка персонажей на фронте
api > apiSlice.js
И по итогу мы теперь можем удалить весь heroesSlice.js, который использовался для реализации управления состояниями
И теперь список персонажей выглядит таком образом:
components > heroesList > HeroesList.js
И сейчас можно сделать следующие выводы:
RTK Query предлагает нам не пользоваться каким-либо единственным хранилищем состояния, а пользоваться активным взаимодействием с сервером для актуализации данных
В браузере же данные хранятся только в кешированном формате (то есть тех данных, что хранится просто в нашем стейте просто нет - они в памяти браузера)