Доброго времени суток, друзья! Предлагаю вашему вниманию результаты небольшого исследования, посвященного сравнению Redux и Vuex. Redux и Vuex — это библиотеки для управления состоянием React и Vue-приложений, соответственно. Каждая из них по-своему реализует архитектуру для создания пользовательских интерфейсов, известную под названием Flux. Обратите внимание: Flux-архитектура предназначена для работы с глобальным или распределенным (global, shared) состоянием, т.е. состоянием, которое используется двумя и более автономными компонентами приложения. Автономными являются компоненты, между которыми не существует отношений или связи “предок-потомок” или “родитель-ребенок”, т.е. это компоненты из разных поддеревьев дерева компонентов. Состояние, которое используется одним компонентом или передается от родительского компонента дочерним и обратно (в пределах одного поддерева), является локальным (local), оно должно храниться и управляться соответствующим компонентом. Разумеется, это не относится к корневому (root) компоненту. Архитектура Flux (в Redux) предполагает следующее: Это выглядит примерно так (без учета селекторов и преобразователей): 
Наша задача состоит в том, чтобы определить, кто справляется с реализацией Flux лучше, Redux или Vuex. Для того, чтобы это выяснить, мы создадим простое приложение — список задач — со следующим функционалом: В первой части статьи мы поговорим (много) про Redux Toolkit и (немного) про React Redux, а также реализуем хранилище для нашего React-приложения, во второй — поговорим о Vuex 4, реализуем хранилище для нашего Vue-приложения, сравним реализацию компонентов приложений и измерим производительность операций, выполняемых с помощью Redux и Vuex. Предполагается, что вы знакомы хотя бы с одним из названных фреймворков для фронтенда. Также предполагается, что вы немного знакомы с Node.js. О самих фреймворках я рассказывать не буду, но почти каждая строка кода компонентов будет снабжена подробным комментарием. Вместо рассказа о фреймворках, каждый из которых имеет документацию на русском языке (правда, документация по React сильно устарела, но тем не менее), я постараюсь дать вам исчерпывающее представление о Redux и Vuex. На выходе вы получите два готовых приложения, написанных с использованием самого последнего синтаксиса двух наиболее популярных фронтенд-фреймворков. В качестве БД мы будем использовать JSON Server, а для стилизации — Bootstrap. Выглядеть наше приложение будет так: Если вас интересует только код, то вот ссылка на репозиторий. Демо React-приложения можно посмотреть здесь, а Vue-приложения — здесь. Вы готовы? Тогда вперед! Redux Toolkit— это библиотека, существенно упрощающая работу с Redux. Она была разработана для решения трех главных проблем, связанных с использованием Redux: В состав Redux Toolkit входит следующее (из того, что мы будем использовать): Не волнуйтесь, мы со всем разберемся. Сначала мы рассмотрим сигнатуру каждого метода, а затем используем их для создания хранилища нашего React-приложения. Метод configureStore() принимает объект со следующими свойствами: Таким образом, единственным обязательным свойством объекта, передаваемого в configureStore(), является reducer: Пример с несколькими редукторами, начальным состоянием и инструментами разработчика Redux, включенными только в режиме для разработки: Метод createSlice() принимает объект со следующими свойствами: createSlice() под капотом использует два других метода — createAction() и createReducer()— для создания операций и редуктора, соответственно, что позволяет использовать библиотеку immer для "мутирования" состояния, т.е. для его прямой модификации. Рассматриваемый метод возвращает такой объект: Каждая функция, переданная в свойство reducers метода createSlice(), становится одноименным создателем операции в свойстве actions возвращаемого объекта. Одной из ключевых концепций Redux является то, что каждый частичный редуктор "владеет" определенной частью состояния, и несколько таких редукторов могут обрабатывать один и тот же тип операции. extraReducers предназначены для обработки внешних по отношению к редукторам операций, например, HTTP-запросов. Простой пример со счетчиком: Если полезная нагрузка, используемая для изменения состояния, требует предварительной подготовки, то значением соответствующего поля свойства reducers должен быть объект, а не функция. Такой объект должен содержать два свойства: reducer и prepare. Значением reducer должна быть функция для изменения состояния, а значением prepare— колбэк для преобразования полезной нагрузки. Полный пример со счетчиком и пользователем: Метод createAsyncThunk() принимает тип операции, колбэк, который должен возвращать промис и объект с настройками. Также этот метод генерирует типы операций, соответствующие жизненному циклу промиса, и возвращает преобразователь (thunk), который запускает колбэк промиса и отправляет в редуктор соответствующие операции. Для обработки этих операций используются дополнительные редукторы. Да, проще показать. Рассмотрим параметры, которые принимает createAsyncThunk() — type, payloadCreator и options. Пример получения данных пользователя по его идентификатору с изменением индикатора загрузки и обеспечением отправки только одного запроса за раз: createEntityAdapter() — это адаптер сущностей, функция, генерирующая набор встроенных редукторов и селекторов для выполнения GRUD-операций с нормализованными данными. Сущность — это уникальный объект, содержащий определенную часть данных. Например, в блоге такими объектами могут быть User, Post и Comment. Каждый объект может иметь несколько экземпляров. Каждый экземпляр должен иметь уникальный идентификатор. Методы, генерируемые createEntityAdapter(), манипулируют нормализованной структурой, которая выглядит так: createEntityAdapter() принимает 2 параметра: Возвращаемым значением рассматриваемого метода является адаптер сущностей, объект, содержащий редукторы, селекторы, selectId, sortComparer и метод для инициализации начального состояния. Простой пример с книгами: Основным содержимым адаптера сущности является набор редукторов для добавления, обновления и удаления экземпляров из объекта состояния: Каждый из указанных методов имеет такую сигнатуру: Другими словами, каждый метод принимает состояние вида {ids: [], entities: {}}, вычисляет новое состояние на основе аргумента и возвращает его. Адаптер сущностей также содержит функцию getSelectors(), возвращающую набор мемоизированных селекторов, которые умеют читать содержимое объекта состояния: Расширенный пример с книгами: createSelector() — это метод из библиотеки Reselect для создания селекторов на основе других селекторов. Он принимает селекторы через запятую или в виде массива и возвращает новый селектор. Этот новый селектор, в свою очередь, принимает состояние, производит выборку с помощью переданных селекторов и вычисляет конечный результат. Сигнатура: Пример: Итак, мы разобрались с методами, предоставляемыми Redux Toolkit, которые мы будем использовать для создания хранилища нашего React-приложения. Далее я предлагаю взглянуть на React Redux. React Redux — это "официальный слой для связывания пользовательского интерфейса React-приложений с Redux". Он позволяет компонентам читать данные из хранилища и отправлять операции в редуктор для обновления состояния. Провайдер Данный компонент делает хранилище доступным для остальной части приложения. Другими словами, он делает состояние приложения доступным в компонентах, независимо от уровня их вложенности. Хуки React Redux предоставляет 2 хука, позволяющих компонентам взаимодействовать с хранилищем: Инициализируем проект: Переходим в директорию проекта и устанавливаем необходимые зависимости: Удаляем ненужные файлы, создаем нужные, приводя проект к такой структуре: Подключаем bootstrap и bootstrap-icons в public/index.html: Создаем в корневой директории файл db.json следующего содержания (наша БД): Каждая задача имеет идентификатор, текст, индикатор выполнения и индикатор редактирования. Добавляем в раздел scripts файла package.json команду для запуска серверов для разработки: одного для БД, другого для React: Также в package.json необходимо добавить такую строку для перенаправления запросов (по умолчанию запросы в режиме для разработки отправляются на localhost:3000): Для того, чтобы убедиться в правильной настройке проекта, можно добавить такой код в файл scr/index.jsx: и выполнить команду: В консоли инструментов разработчика должно появиться такое сообщение: Перед тем, как приступать к непосредственной реализации хранилища, подумаем о том, как оно должно выглядеть. Состояние Первый вопрос, который необходимо решить: что из себя будет представлять состояние нашего приложения. Очевидно, что нам потребуется состояние для задач, в котором, кроме самих задач, могут храниться индикатор загрузки и текст сообщения. Также нам потребуется отдельное состояние для фильтра, изменение которого будет влиять на список отображаемых задач. Начальным значением фильтра будет all — отображение всех задач. Операции Второй вопрос: какие операции потребуются для реализации функционала нашего приложения. В случае с фильтром нам нужна всего лишь одна операция — операция для установки значения фильтра. Назовем ее setFilter(). Что касается задач, то нам потребуются следующие операции: Преобразователи Поскольку мы будем получать и сохранять задачи в БД, нам потребуется 2 преобразователя: fetchTodos() и saveTodos(). Также, поскольку мы хотим показывать пользователю сообщение о загрузке, сохранении задач или о возникшей ошибке в течение определенного времени, нам понадобится еще один преобразователь — для реализации задержки перед очисткой сообщения. Назовем его giveMeSomeTime(). Таким образом, у нас будет 3 асинхронных операции. Для обработки их результатов нам нужны 3 дополнительных редуктора. Однако, учитывая, что мы хотим переключать индикатор загрузки при получении и сохранении задач в БД, у нас будет не 3, а 5 дополнительных редукторов: Селекторы Список отображаемых задач будет зависеть от состояния фильтра. Для выборки отфильтрованных задач мы будем использовать селектор selectFilteredTodos(). Для сбора статистики нам потребуется еще один селектор. Назовем его selectTodoStats(). Пожалуй, это все, что нам нужно для настройки хранилища. Можно приступать к его реализации. Обратите внимание: обычно, для каждой части состояния создается отдельный файл, например, todoSlice.js, filterSlice.js и т.д. Однако, учитывая небольшой размер кодовой базы, мы ограничимся одним файлом — src/store/index.js. Импортируем необходимые инструменты: Определяем константу для адреса сервера: Создаем адаптер сущностей для задач: Инициализируем начальное состояние для задач: Создаем и экспортируем преобразователь для получения задач от сервера: Создаем и экспортируем преобразователь для сохранения задач в БД. Реализация этого преобразователя стала для меня интересной задачей. Я пытался свести к минимуму взаимодействие приложения с сервером и ограничиться двумя асинхронными операциями: получение задач от сервера и сохранение (текущего состояния) задач в БД. Все остальные операции выполняются синхронно и локально. С первой асинхронной операцией все просто: отправляем запрос, получаем ответ. Но в случае с сохранением задач в БД ситуация несколько сложнее по следующим причинам: Я постарался свести количество выполняемых запросов к минимуму. Вот что у меня получилось: Создаем и экспортируем преобразователь для задержки в 2 секунды. Искусственную задержку для очистки сообщения также можно реализовать с помощью хука useEffect() в App.jsx, но мне хотелось управлять всем состоянием централизованно. И да, это своего рода костыль. Создаем часть состояния для задач: Экспортируем операции для работы с задачами: Определяем начальное состояние, часть состояния для фильтра и экспортируем операцию для работы с ним: Создаем и экспортируем селекторы: Наконец, создаем и экспортируем хранилище: Для того, чтобы сделать состояние из хранилища доступным в компонентах приложения, а также обновить состояние задачами, полученными от сервера, необходимо добавить такой код в src/index.jsx: Таким образом, современный Redux в лице Redux Toolkit предоставляет относительно простой и понятный (по крайней мере, по сравнению с оригинальным Redux) функционал для управления состоянием приложения. Но является ли Redux Toolkit апогеем развития Flux-архитектуры? Посмотрим, что на этот счет скажет Vuex, но уже в следующей части статьи. Благодарю за внимание и хорошего дня!  JavaScript Developer