Redux toolkit что это
Redux — средний уровень. Использование redux-toolkit
В этом уроке мы продолжим работу и усовершенствуем наше предыдущее приложение. Мы расширим наш store и добавим немного функциональности, разберемся с пакетом redux-toolkit.
По ходу урока мы добавим к нашему приложению категории, сделаем фильтрацию тасков, изменим структуру файлов и перепишем экшены и редюсеры с помощью redux-toolkit.
Ссылка на прошлый проект:
Новое приложение будет выглядеть вот так — https://codesandbox.io/s/github/web-devguru/redux-toolkit-example (ссылки на исходники внизу поста)
Готовое приложение
Структура папок и файлов для Redux Store
Есть несколько подходов для структурирования файлов в вашем приложении. Наверняка когда у вас будет большой опыт разработки вы сможете выбрать для себя наиболее удобный. Сейчас мы рассмотрим 2 варианта, которые советуют сами разработчики редакса.
Ducks Pattern
Этот вариант предлагает хранение экшенов и редюсеров в отдельных файлах по типу, к чему они относятся. Например, то что мы писали в прошлый раз можно сохранить в один файл todos.js и определить в нем сразу и экшены и редюсер. В таком случае структура выглядела бы так:
сategories.js — отвечает за экшены и редюсер для категорий
tasks.js — отвечает за экшены и редюсер для тасков
сonfigureStore.js — отвечает за создание стора
entities.js — этот файл будет объединять categories и todos в одну запись в store
rootReducer.js — в этом файле объединяются 2 редюсера (категории и таски) в один
Feture Folder
Данный подход предполагает для каждой отдельной функции создавать отдельную папку и в нее помещать отдельно actions, reducers.
Мы будем строить приложение используя этот паттерн. В таком случае наша часть приложения, отвечающая за store, будет выглядеть следующим образом:
Так в каждой папке находятся actions/reducer отвечающие за эту часть нашего хранилища.
Файлы entities.js и filtersReducers.js будут отвечать за объединение тасков/категорий и фильтров соответственно в отдельные записи в store. В rootReducer мы потом объединим их оба.
Как и говорил в начале, мы добавим в наше приложение немного новой функциональности. В следующих разделах мы постепенно будем разбираться какие инструменты нам дает redux-toolkit и будем переделывать наше приложение.
Подготовка проекта
Так как наш проект будет постепенно становиться больше, давайте создадим в папке components отдельную папку для каждого компонента и файл index.js, который будет отвечать только за экспорт нашего компонента. Вот так будет выглядеть структура нашего приложения:
На примере, компонента AddNewTask файл index.js будет выглядеть так:
Мы просто экспортируем по дефолту то, что было экспортировано из файла AddNewTask.js. Это делается для того, чтобы если вы решите изменить экспорт или имя компонента вам не приходилось его везде менять. У вас в нужном месте импорт останется тот же, из файла index.js.
Также и для store давайте создадим папки: categories, categoryFilter, statusFilter, tasks. В каждой папке будет 2 файла — actions.js и reducer.js
Файлы в папке store можно удалить — actions.js, actionsTypes.js, reducer.js
Вам нужно сделать такие же папки и файлы в своем приложении, также не забудьте изменить путь там где эти модули импортируются. Или можете скачать уже готовое приложение по ссылкам в конце поста.
Дальше по ходу урока мы изменим эти файлы.
Redux Toolkit
Данная библиотека позволяет упростить работу с созданием экшенов, редюсеров, позволяет комбинировать несколько редюсеров… в общем упрощает работу разработчика с Redux (документация).
Инструменты, которые нам предлагает redux-toolkit
Для установки этого пакета мы можем воспользоваться следующей командой:
createAction()
В прошлом посте мы рассматривали как создаются экшены при использовании редакса — мы определяли типы экшенов, потом создавали функцию и возвращали из нее объект со свойствами
При использовании redux-toolkit, мы можем создать экшен проще. В файле src/store/tasks/actions.js заменим код на:
В качестве параметра для функции createAction нам необходимо передать текст, который будет использоваться в качестве type.
Таким же образом давайте создадим экшены для наших категорий. В файле src/store/categories/actions.js добавим следующий код:
Также создадим экшены для наших будущих фильтров (по статусу и по категории таска).
Сейчас мы добавим код для экшенов, а в следующем разделе добавим редюсеры.
categoryFilters.SHOW_ALL мы передаем как значение по умолчанию. Ноль указываем, так как в дальнейшем будем передавать в редюсер id категории, чтобы везде было цифровое значение.
createReducer()
Этот инструмент упрощает нам работу с созданием редюсеров. Первое и самое основное это то, что тут можно не переживать по поводу «мутабельности» вашего кода, т.е. вы можете изменять state напрямую, а дальше createReducer() сделает всё за вас.
Файл src/store/tasks/reducer.js будет таким
createReducer() принимает 2 параметра: дефолтное состояние стора, в нашем случае это пустой массив, и объект с имеющимися экшенами и колбеком. Колбэк также принимает текущее состояние и action в качестве параметров.
Так как мы создавали экшены через createAction(), теперь мы можем использовать их как в коде выше — [addTask.type] (будет подставлено, то значение, которое мы передавали как параметр для createAction(«TASK_ADD»))
Теперь давайте рассмотрим редюсер для категорий, тут мы еще будем передавать начальное значение.
Как вы видите тут мы в качестве значения по умолчанию передаем массив категорий — defaultCategories, а также lastId равен 3, т.к. у нас уже есть 3 категории и при добавлении новой нумерация продолжится с 4.
Теперь добавим код для фильтров.
createSlice()
Данный инструмент позволяет создать actions и reducers с помощью всего одной функции. Мы его использовать не будем, но рассмотрим как он работает.
В случае с нашими ToDo мы бы сделали вот так:
combineReducers()
Т.к. у нас уже есть 2 редюсера, нам теперь их нужно скомбинировать. Для этого мы воспользуемся инструментом combineReducers().
Создаем в папке store три файла — entities.js, filtersReducer.js, rootReducer.js
В файл entities.js с добавим следующий код:
Теперь нужно объединить фильтры в отдельный кусок в стор. Для этого создадим файл src/store/filtersReducer.js
Давайте создадим еще файл rootReducer.js, который будет отвечать за объединение всех существующих редюсеров:
Теперь в нашем store будет отдельная запись entities, где будут хранится категории и таски, а также filters, где будут хранится фильтры выбранные пользователем:
configureStore()
В прошлом уроке мы использовали createStore() функцию для создания нашего хранилища. Также мы добавляли еще один параметр для корректной работы плагина Redux DevTools — плагина для браузера, это была вот такая строка:
При использовании configureStore() нам эта строка не понадобится. Чтобы использовать этот инструмент, нам нужно его импортировать
Создание новых компонентов
Теперь нам нужно добавить новые компоненты. В src/components создаем папки AddNewCategory, CategoriesList, Category. Теперь поочередно добавим в них нужный код.
Следующим этапом нам нужно создать компонент для фильтрации по статусу. Создадим в папке componetns папку StatusFilter и в ней 2 файла — index.js и StatusFilter.js
Изменение старых компонентов
После изменений, которые мы сделали выше, нужно изменить наши существующие компоненты.
В первую очередь нужно все файлы перенести в соответствующие папки, добавить файл index.js с аналогичным содержимым как мы сделали для категорий.
В src/components/Task/Task.js нужно изменить метод dispatch и передать туда объект со свойством id и значением id. Это нужно изменить на 11 строке и 20-й:
Также изменим импорт экшенов в AddNewTask на строке 3 на такой:
Т.к. у нас изменилась структура стора, добавились новые компоненты, нам нужно изменить и файл App.js
В этом файле мы убрали получение категорий, мы раньше перенесли его в файл CategoriesList.js, также добавили селектор для выборки тасков для отображения. Что такое селекторы мы рассмотрим ниже.
Изменения стилей
Я изменил стили и добавил в конце файла App.css:
Selectors (селекторы)
Селекторы это простые функции, которые возвращают, в нашем случае из стора, нужную нам информацию. К примеру нам нужно выбрать из стора все таски, для этого мы пишем функцию, которая на вход получит весь стор, а вернет нам только tasks.
В redux toolkit есть своя функция (точнее она используется из библиотеки Reselect), которая в добавок к этому делает Memoization (не знаю, как даже правильно это будет на русском, lol). Т.е. результат кешируется и в дальнейшем если в сторе не было изменений, то будут использованы данные из памяти.
Для нашего приложения нам пока нужен будет только один селектор, для выбора тасков с учетом фильтрации (по статусу и категории). Создадим файл в src/store/selectors.js
Готовое приложение и ссылки на его скачивание:
Redux toolkit что это
Redux Toolkit is our official, opinionated, batteries-included toolset for efficient Redux development. It is intended to be the standard way to write Redux logic, and we strongly recommend that you use it.
It includes several utility functions that simplify the most common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire «slices» of state at once without writing any action creators or action types by hand. It also includes the most widely used Redux addons, like Redux Thunk for async logic and Reselect for writing selector functions, so that you can use them right away.
Installation
Redux Toolkit is available as a package on NPM for use with a module bundler or in a Node application:
Purpose
The Redux core library is deliberately unopinionated. It lets you decide how you want to handle everything, like store setup, what your state contains, and how you want to build your reducers.
This is good in some cases, because it gives you flexibility, but that flexibility isn’t always needed. Sometimes we just want the simplest possible way to get started, with some good default behavior out of the box. Or, maybe you’re writing a larger application and finding yourself writing some similar code, and you’d like to cut down on how much of that code you have to write by hand.
Redux Toolkit was originally created to help address three common concerns about Redux:
Why You Should Use Redux Toolkit
Redux Toolkit makes it easier to write good Redux applications and speeds up development, by baking in our recommended best practices, providing good default behaviors, catching mistakes, and allowing you to write simpler code. Redux Toolkit is beneficial to all Redux users regardless of skill level or experience. It can be added at the start of a new project, or used as part of an incremental migration in an existing project.
Note that you are not required to use Redux Toolkit to use Redux. There are many existing applications that use other Redux wrapper libraries, or write all Redux logic «by hand», and if you still prefer to use a different approach, go ahead!
Overall, whether you’re a brand new Redux user setting up your first project, or an experienced user who wants to simplify an existing application, using Redux Toolkit will make your code better and more maintainable.
What’s Included
Redux Toolkit includes:
Redux Toolkit also has a new RTK Query data fetching API. RTK Query is a powerful data fetching and caching tool built specifically for Redux. It is designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
Redux Toolkit больше не нужен?
История появления
Наша компания занимается разработкой проектов для стартапов по всему миру. А для этих сумасшедших вдохновенных ребят самым важным (или один из таковых) является время. Раньше вышел на рынок — выше шансы выжить. Поэтому мы всегда стараемся сократить время разработки, не жертвуя при этом качеством, и уменьшить количество ошибок, вызванных человеческим фактором. По большей части выбираем что-то стандартное и более или менее стабильное. В данном случае ничего готового найти не удалось, поэтому засучили рукава и начали смотреть, что тут можно навелосипедить.
Как водится, начали с описания проблемы и получили следующее:
Пока ПМ-ы ломали головы над тем, откуда взять еще пару другую часов в сутках, нашлась пара энтузиастов, которые сгенерировали и воплотили в жизнь простую до банальности идею. Да-да, гениальные идеи, после того как придумываются, всегда кажутся простыми. А идея заключалась в следующем: просто-напросто генерировать отдельные куски кода вместо того, чтобы переписывать его вручную. Использовать сниппеты и получать на выходе простыни не хотелось, т.к. любые изменения приводили к катастрофе и повторным рефакторингам, поэтому по-быстрому запилили функцию, которая принимала на вход некие параметры и строила reducer, action и saga. Так родилась первая версия коммуникаций (@axmit/redux-communications). Название “коммуникации” родилось как-то само собой, т.к. эта библиотека связывает воедино стор, саги и компоненты. Так и пошло …
Сразу после этого наступило счастье. Ну или почти сразу. Время разработки уменьшилось примерно на 20% (как мы это посчитали — тема отдельной статьи). В первую очередь — за счет значительного сокращения количества вылезающих багов. И это не говоря о том, что разработчики стали гораздо реже допускать глупые ошибки по невнимательности.
Линус Торвальдс как-то сказал: «Болтовня ничего не стоит. Покажите мне код.», и я полностью с ним согласен. Не буду цитировать и переписывать доку или приводить здесь простыни кода, приведу лишь небольшой пример. Ссылки на полное описание библиотеки и песочницу с примерами также можно найти в конце статьи.
Рассмотрим типичную задачу — нам нужно создать CRUD по какой-то сущности. Возьмем для примера задачу(task). Описывать стандартный вариант считаю бесполезным, т.к. он займет много места, и все, кто сталкивался с редаксом, скорее всего примерно представляют, как это будет выглядеть. А чтобы получить коммуникацию, нужно сделать следующие вещи:
1. Описать транспорт, без этого никуда
2. Описать имя namespace
3. Создать стратегию для создания коммуникации
4. Создать коммуникацию
5. Подключить редьюсеры и саги из коммуникации, как обычно
6. После этого остается подключить стор к компоненту
7. И начать пользоваться
По сути и транспорт и компонент нужно будет создавать в любом случае, а полный код коммуникации выглядит следующим образом:
Это все, что нужно сделать, чтобы иметь полностью работоспособный CRUD. Если нужно сделать что-то более сложное — можно расширить CRUD коммуникацию или использовать BaseCommunication. В крайнем случае, под капотом это все тот же старый-добрый редакс со всеми его возможностями. Гибкость не пострадала. Транспорт вынесен в отдельный слой, и его реализация может быть какой угодно. У нас на проектах есть graphQL и простые запросы с использованием axios, и трудностей в этом плане мы не испытывали. Внимательный читатель мог заметить, что библиотека экспортирует саги, и это является одним из самых существенных ее ограничений. Если по какой-то причине вы не можете использовать саги, данная библиотека, к сожалению, вам не подходит.
Почему сейчас?
Решение написать статью пришло после прочтения этой статьи. Потыкав данный инструмент, с удивлением для себя обнаружил, что коммуникации намного проще, более лаконичны, дают более четкую структуру стора и при этом не уступают в гибкости. Посидев и поразбиравшись часок с исходниками из примера к redux-toolkit, переписал его на коммуникации. Старался вносить минимум изменений, чтобы было проще отследить разницу, хотя, на мой взгляд, структура примера очень запутана. Комментарии по коду специально оставил, чтобы проще было сравнивать как было и как стало. Обратите внимание на файлы *.communication.ts и слайсы, которые они заменяют.
То, что количество строк намного меньше и сам код гораздо приятнее выглядит(субъективно) не столь важно, т.к. в проде у нас бывают довольно тяжеловесные коммуникации. Тут есть одно более важное отличие. Код — декларативный. Мы просто определяем, что и откуда хотим получить и что сделать с данными, а как это сделать — нас совершенно не волнует.
Вывод — redux-toolkit стоит можно использовать для кастомизации, для всего остального есть MasterCa. @axmit/redux-communications.
Подведем итоги
Полное описание библиотеки можно найти тут, а потыкать online тут.
Полный код переписанного на коммуникации примера из доки ReduxToolkit тут, а потыкать можно тут.
Redux Vs Vuex. Часть 1
Доброго времени суток, друзья!
Введение
Обратите внимание: Flux-архитектура предназначена для работы с глобальным или распределенным (global, shared) состоянием, т.е. состоянием, которое используется двумя и более автономными компонентами приложения. Автономными являются компоненты, между которыми не существует отношений или связи “предок-потомок” или “родитель-ребенок”, т.е. это компоненты из разных поддеревьев дерева компонентов. Состояние, которое используется одним компонентом или передается от родительского компонента дочерним и обратно (в пределах одного поддерева), является локальным (local), оно должно храниться и управляться соответствующим компонентом. Разумеется, это не относится к корневому (root) компоненту.
Архитектура Flux (в Redux ) предполагает следующее:
Это выглядит примерно так (без учета селекторов и преобразователей):
Для того, чтобы это выяснить, мы создадим простое приложение — список задач — со следующим функционалом:
На выходе вы получите два готовых приложения, написанных с использованием самого последнего синтаксиса двух наиболее популярных фронтенд-фреймворков.
Выглядеть наше приложение будет так:
Если вас интересует только код, то вот ссылка на репозиторий.
Демо React-приложения можно посмотреть здесь, а Vue-приложения — здесь.
Вы готовы? Тогда вперед!
Redux Toolkit
В состав Redux Toolkit входит следующее (из того, что мы будем использовать):
Не волнуйтесь, мы со всем разберемся. Сначала мы рассмотрим сигнатуру каждого метода, а затем используем их для создания хранилища нашего React-приложения.
configureStore()
Метод configureStore() принимает объект со следующими свойствами:
createSlice()
Метод createSlice() принимает объект со следующими свойствами:
createSlice() под капотом использует два других метода — createAction() и createReducer() — для создания операций и редуктора, соответственно, что позволяет использовать библиотеку immer для «мутирования» состояния, т.е. для его прямой модификации.
Рассматриваемый метод возвращает такой объект:
Одной из ключевых концепций Redux является то, что каждый частичный редуктор «владеет» определенной частью состояния, и несколько таких редукторов могут обрабатывать один и тот же тип операции. extraReducers предназначены для обработки внешних по отношению к редукторам операций, например, HTTP-запросов.
Простой пример со счетчиком:
Полный пример со счетчиком и пользователем:
createAsyncThunk()
Метод createAsyncThunk() принимает тип операции, колбэк, который должен возвращать промис и объект с настройками. Также этот метод генерирует типы операций, соответствующие жизненному циклу промиса, и возвращает преобразователь (thunk), который запускает колбэк промиса и отправляет в редуктор соответствующие операции. Для обработки этих операций используются дополнительные редукторы. Да, проще показать.
Пример получения данных пользователя по его идентификатору с изменением индикатора загрузки и обеспечением отправки только одного запроса за раз:
createEntityAdapter()
createEntityAdapter() — это адаптер сущностей, функция, генерирующая набор встроенных редукторов и селекторов для выполнения GRUD-операций с нормализованными данными.
createEntityAdapter() принимает 2 параметра:
Простой пример с книгами:
Основным содержимым адаптера сущности является набор редукторов для добавления, обновления и удаления экземпляров из объекта состояния:
Каждый из указанных методов имеет такую сигнатуру:
Расширенный пример с книгами:
createSelector()
createSelector() — это метод из библиотеки Reselect для создания селекторов на основе других селекторов. Он принимает селекторы через запятую или в виде массива и возвращает новый селектор. Этот новый селектор, в свою очередь, принимает состояние, производит выборку с помощью переданных селекторов и вычисляет конечный результат.
React Redux
React Redux — это «официальный слой для связывания пользовательского интерфейса React-приложений с Redux». Он позволяет компонентам читать данные из хранилища и отправлять операции в редуктор для обновления состояния.
Провайдер
Данный компонент делает хранилище доступным для остальной части приложения. Другими словами, он делает состояние приложения доступным в компонентах, независимо от уровня их вложенности.
Хуки
React Redux предоставляет 2 хука, позволяющих компонентам взаимодействовать с хранилищем:
Хранилище React-приложения
Создание и настройка проекта
Переходим в директорию проекта и устанавливаем необходимые зависимости:
Удаляем ненужные файлы, создаем нужные, приводя проект к такой структуре:
Подключаем bootstrap и bootstrap-icons в public/index.html :
Создаем в корневой директории файл db.json следующего содержания (наша БД):
Каждая задача имеет идентификатор, текст, индикатор выполнения и индикатор редактирования.
Добавляем в раздел scripts файла package.json команду для запуска серверов для разработки: одного для БД, другого для React :
Также в package.json необходимо добавить такую строку для перенаправления запросов (по умолчанию запросы в режиме для разработки отправляются на localhost:3000 ):
Для того, чтобы убедиться в правильной настройке проекта, можно добавить такой код в файл scr/index.jsx :
и выполнить команду:
В консоли инструментов разработчика должно появиться такое сообщение:
Проектирование хранилища
Перед тем, как приступать к непосредственной реализации хранилища, подумаем о том, как оно должно выглядеть.
Состояние
Первый вопрос, который необходимо решить: что из себя будет представлять состояние нашего приложения.
Очевидно, что нам потребуется состояние для задач, в котором, кроме самих задач, могут храниться индикатор загрузки и текст сообщения.
Также нам потребуется отдельное состояние для фильтра, изменение которого будет влиять на список отображаемых задач. Начальным значением фильтра будет all — отображение всех задач.
Операции
Второй вопрос: какие операции потребуются для реализации функционала нашего приложения.
Что касается задач, то нам потребуются следующие операции:
Преобразователи
Таким образом, у нас будет 3 асинхронных операции. Для обработки их результатов нам нужны 3 дополнительных редуктора. Однако, учитывая, что мы хотим переключать индикатор загрузки при получении и сохранении задач в БД, у нас будет не 3, а 5 дополнительных редукторов:
Селекторы
Пожалуй, это все, что нам нужно для настройки хранилища. Можно приступать к его реализации.
Реализация хранилища
Импортируем необходимые инструменты:
Определяем константу для адреса сервера:
Создаем адаптер сущностей для задач:
Инициализируем начальное состояние для задач:
Создаем и экспортируем преобразователь для получения задач от сервера:
Создаем и экспортируем преобразователь для сохранения задач в БД.
Реализация этого преобразователя стала для меня интересной задачей. Я пытался свести к минимуму взаимодействие приложения с сервером и ограничиться двумя асинхронными операциями: получение задач от сервера и сохранение (текущего состояния) задач в БД. Все остальные операции выполняются синхронно и локально. С первой асинхронной операцией все просто: отправляем запрос, получаем ответ. Но в случае с сохранением задач в БД ситуация несколько сложнее по следующим причинам:
Я постарался свести количество выполняемых запросов к минимуму. Вот что у меня получилось:
Создаем и экспортируем преобразователь для задержки в 2 секунды.
Создаем часть состояния для задач:
Экспортируем операции для работы с задачами:
Определяем начальное состояние, часть состояния для фильтра и экспортируем операцию для работы с ним:
Создаем и экспортируем селекторы:
Наконец, создаем и экспортируем хранилище:
Для того, чтобы сделать состояние из хранилища доступным в компонентах приложения, а также обновить состояние задачами, полученными от сервера, необходимо добавить такой код в src/index.jsx :