архитектура react redux приложения

Архитектура Redux. Да или нет?

Автор материала, перевод которого мы сегодня публикуем, говорит, что входит в команду мессенджера Hike, которая занимается новыми возможностями приложения. Цель этой команды заключается в том, чтобы воплощать в реальность и исследовать идеи, которые могут понравиться пользователям. Это означает, что действовать разработчикам нужно оперативно, и что им приходится часто вносить изменения в исследуемые ими новшества, которые направлены на то, чтобы сделать работу пользователей как можно более удобной и приятной. Они предпочитают проводить свои эксперименты с применением React Native, так как эта библиотека ускоряет разработку и позволяет использовать один и тот же код на разных платформах. Кроме того, они пользуются библиотекой Redux.

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

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

Разделение ответственностей

Что такое «разделение ответственностей»? Вот что говорит об этом Википедия: «В информатике разделение ответственностей представляет собой процесс разделения компьютерной программы на функциональные блоки, как можно меньше перекрывающие функции друг друга. В более общем случае, разделение ответственностей — это упрощение единого процесса решения задачи путём разделения на взаимодействующие процессы по решению подзадач».

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

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.
Архитектура Redux

Вот краткая характеристика этих блоков:

Что делать, если разным компонентам нужны одни и те же данные?

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

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.
Экран с информацией о друзьях в приложении Hike

Здесь имеется 3 React-компонента:

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

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

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.
Экран чатов в приложении Hike

Предположим, в приложении имеется экран чатов, который также содержит список друзей. Видно, что и на экране со списком друзей, и на экране чатов используются одни и те же данные. Как поступить в подобной ситуации? У нас есть два варианта:

Использование Redux

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

▍1. Хранилище данных

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

▍2. Создатели действий

В данном случае создатель действия используется для диспетчеризации событий, направленных на сохранение и обновление данных о друзьях. Вот код файла friendsActions.js:

▍3. Редьюсеры

Редьюсеры ожидают поступления событий, представляющих диспетчеризованные действия, и обновляют данные о друзьях. Вот код файла friendsReducer.js:

▍4. Компонент, выводящий список друзей

Этот компонент-контейнер просматривает данные о друзьях и обновляет интерфейс при их изменении. Кроме того, он ответственен за загрузку данных из хранилища в том случае, если их у него нет. Вот код файла friendsContainer.js:

▍5. Компонент, выводящий список чатов

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

О реализации архитектуры Redux

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

Тестирование

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

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

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

Redux — это замечательно, но используя эту технологию мы столкнулись с некоторыми трудностями.

Трудности при использовании Redux

▍Избыток шаблонного кода

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

▍Хранилище Redux — это синглтон

В Redux хранилище данных построено с использованием паттерна «синглтон», хотя компоненты могут иметь несколько экземпляров. Чаще всего это не проблема, но в определённых ситуациях подобный подход к хранению данных может создавать некоторые сложности. Например, представим себе, что существуют два экземпляра некоего компонента. Когда в любом из этих экземпляров меняются данные, эти изменения сказываются и на другом экземпляре. В определённых случаях такое поведение может оказаться нежелательным, может понадобиться, чтобы каждый экземпляр компонента пользовался бы собственной копией данных.

Итоги

Вспомним наш главный вопрос, который заключается в том, стоит ли тратить время и силы на реализацию архитектуры Redux. Мы, в ответ на этот вопрос, говорим Redux «да». Эта архитектура помогает экономить время и силы при разработке и развитии приложений. Использование Redux облегчает жизнь программистов при необходимости частого внесения изменений в приложение, упрощает тестирование. Конечно, архитектура Redux предусматривает наличие немалого объёма шаблонного кода, но она способствует разбиению кода на модули, с которыми удобно работать. Каждый такой модуль может быть протестирован независимо от других, что содействует выявлению ошибок ещё на этапе разработки и позволяет обеспечить высокое качество программ.

Уважаемые читатели! Пользуетесь ли вы Redux в своих проектах?

Источник

Архитектура модульных React + Redux приложений 2. Ядро

В первой части я уделил внимание только общей концепции: редюсеры, компоненты и экшны чаще меняются одновременно, а не по отдельности, поэтому и группировать и их целесообразнее по модулям, а не по отдельным папкам actions, components, reducers. Также к модулям были предъявлены требования:

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

Например, если мы хотим создать стандартный CRUD над сущностью в БД логично организовать модули так:

Считаем, что для create и update используются стандартный компонент формы, а для вывода данных стандартный компонент Grid из ядра системы, поэтому достаточно определить только модули для этих операций.

Родительский модуль отвечает за вывод лейаута, ссылок «создать», «назад к списку» и сообщений об успешности или не успешности запросов к серверу. Index – за фильтрацию, пагинацию и ссылки. Create и Update выводят формы на создание и редактирование.

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

Для роутов

И для реюсеров

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

Соответствие роутов и стейта

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

Это позволит в редюсере App (если такой нужен) обрабатывать любые события приложения и модифицировать состояние любого дочернего модуля. Пожалуй, это слишком круто для любого, даже самого крутого редюсера. Я не рекомендую вообще переопределять reduce для родительского модуля приложения. Однако, такой редюсер может быть полезен для системных операций.

С роутингом покончено, осталось «законектить» компоненты к стейту. Так как редюсеры скомпонованы рекурсивно в соответствие со вложенностью дочерних модулей коннектить будем также. Здесь все просто. Реализацию mapDispatchToProps рассмотрим чуть ниже.

Компоненты ядра

Итак, ModuleBase – первая и неотъемлемая часть ядра. Без него свой код к приложению вообще не подцепить. ModuleBase предоставляет следующее API:

Компоненты и контейнеры

Контейнеры – один из широко распространённых паттернов в React. Если коротко, то разница между компонентами и контейнерами в следующем:

Функция connect (react-redux) по сути является фабрикой контейнеров.

Для разработки DataGridModule нам потребуются:

Примеси (mixin)

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

Расширим возможности компоновки компонентов и контейнеров с помощью mixin’ов. class и extends – это объекты первого класса в ES6. Иными словами, запись const Enhanced = superclass => class extends superclass корректна. Это возможно, благодаря системе наследования JavaScript, основанной на прототипах.

Добавим в ядро функцию mix и примеси Preloader и ServerData :

Первый проверяет все ключи в стейте и если находит хотя-бы один с определенным свойством isFetching: true выводит поверх компонента диммер. Если кроме isFetching в объекте свойств нет, считаем, что они должны прийти с сервера и вообще не отображаем компонент (считаем не инициализированным).

queryFor

Допишем «обогощалку» для стейта, требующего серверных данных:

Теперь достаточно знать правила использования миксина, чтобы сделать из любого компонента, работающего с клиентскими данными на серверный. Достаточно правильно настроить initialState и подключить mixin.

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

ServerData.reducerFor

Добавляем модуль с гридом в приложение

Если вы дочитали до конца, то FromModuleBase сможете реализовать по аналогии.

Источник

Архитектура модульных React + Redux приложений 2. Ядро

В первой части я уделил внимание только общей концепции: редюсеры, компоненты и экшны чаще меняются одновременно, а не по отдельности, поэтому и группировать и их целесообразнее по модулям, а не по отдельным папкам actions, components, reducers. Также к модулям были предъявлены требования:

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

Например, если мы хотим создать стандартный CRUD над сущностью в БД логично организовать модули так:

Считаем, что для create и update используются стандартный компонент формы, а для вывода данных стандартный компонент Grid из ядра системы, поэтому достаточно определить только модули для этих операций.

Родительский модуль отвечает за вывод лейаута, ссылок «создать», «назад к списку» и сообщений об успешности или не успешности запросов к серверу. Index – за фильтрацию, пагинацию и ссылки. Create и Update выводят формы на создание и редактирование.

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

Для роутов

И для реюсеров

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

Соответствие роутов и стейта

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

Это позволит в редюсере App (если такой нужен) обрабатывать любые события приложения и модифицировать состояние любого дочернего модуля. Пожалуй, это слишком круто для любого, даже самого крутого редюсера. Я не рекомендую вообще переопределять reduce для родительского модуля приложения. Однако, такой редюсер может быть полезен для системных операций.

С роутингом покончено, осталось «законектить» компоненты к стейту. Так как редюсеры скомпонованы рекурсивно в соответствие со вложенностью дочерних модулей коннектить будем также. Здесь все просто. Реализацию mapDispatchToProps рассмотрим чуть ниже.

Компоненты ядра

Итак, ModuleBase – первая и неотъемлемая часть ядра. Без него свой код к приложению вообще не подцепить. ModuleBase предоставляет следующее API:

Компоненты и контейнеры

Контейнеры – один из широко распространённых паттернов в React. Если коротко, то разница между компонентами и контейнерами в следующем:

Функция connect (react-redux) по сути является фабрикой контейнеров.

Для разработки DataGridModule нам потребуются:

Примеси (mixin)

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

Расширим возможности компоновки компонентов и контейнеров с помощью mixin’ов. class и extends – это объекты первого класса в ES6. Иными словами, запись const Enhanced = superclass => class extends superclass корректна. Это возможно, благодаря системе наследования JavaScript, основанной на прототипах.

Добавим в ядро функцию mix и примеси Preloader и ServerData :

Первый проверяет все ключи в стейте и если находит хотя-бы один с определенным свойством isFetching: true выводит поверх компонента диммер. Если кроме isFetching в объекте свойств нет, считаем, что они должны прийти с сервера и вообще не отображаем компонент (считаем не инициализированным).

queryFor

Допишем «обогощалку» для стейта, требующего серверных данных:

Теперь достаточно знать правила использования миксина, чтобы сделать из любого компонента, работающего с клиентскими данными на серверный. Достаточно правильно настроить initialState и подключить mixin.

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

ServerData.reducerFor

Добавляем модуль с гридом в приложение

Если вы дочитали до конца, то FromModuleBase сможете реализовать по аналогии.

Источник

Архитектура модульных React + Redux приложений

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

Большинство разработчиков начинает знакомство с Redux с Todo List Project. Это приложение имеет следующую структуру:

На первый взгляд такая организация кода кажется логичной, ведь она напоминает стандартные соглашения многих backend MVC-фреймворков:

На самом деле, это неудачный выбор как для MVC, так и для React+Redux приложений по следующим причинам:

Мы достаточно давно пришли к такому-же выводу в бекэнд-разработке., поэтому во фронтэнде поступаем также. В русском языке нет подходящего перевода для слова feature как единицы функциональности. Вместо него мы употребляем слово «модуль». В ES6 термин «модуль» имеет другое значение. Чтобы не путать их между собой в случае неоднозначности можно использовать словосочетание «модуль приложения». В повседневной работе сложностей не возникало, кроме этого термин «модуль» хорошо понятен и подходит для коммуникации с бизнес-пользователями.

Модульная структура

Мо́дуль — функционально законченный фрагмент программы.

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

Модульное приложение в моем понимании должно отвечать следующим требованиям:

index.js

Root.js

Пока все достаточно просто. Нам осталось подключить модульную систему к состоянию (store) и настроить роутинг.

defineModule

Напишем небольшую функцию:

Создадим в папке modules модуль личного кабинета пользователя.

Profile/Profile.js

Profile/index.js

И зарегистрируем модуль в файле modules/index.js

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

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

Роутер

А с роутером будет немного сложнее. В общем случае должна быть возможность ассоциировать с модулем более одного URL. Например /profile содержит основную информацию о профиле, а /profile/transactions – список транзакций пользователя. Допустим Мы хотим всегда выводить имя пользователя в личном кабинете, а ниже вывести компонент с двумя табами: «общая информация» и «транзакции».

Тогда, логичная структура роутов будет такой:

Компонент Profile будет выводить имя пользователя и табы, а Info и Transactions – детали профиля и список транзакций соответственно. Но необходимо также поддерживать вариант, когда компоненты модуля не нуждаются в дополнительном группирующем модуле (например, список заказ и окно просмотра заказа являются независимыми страницами).

Введем соглашение

Из модуля можно экспортировать объект структурой как возвращаемый из функции defineModule или массив таких объектов. Все компоненты будут добавлены в список роутов без дополнительной вложенности.

Воспользуемся моноидальной природой списка и получим плоский массив модулей с учетом возможности экспортировать массив или объект.

Таким образом добавление модуля в файл modules/index.js будет автоматически инициализировать новые роуты. Если разработчик забудет объявить роут или запутается в соглашениях, то увидит в консоли недвусмысленное сообщение об ошибке.

onEnter

Обратите внимание на то, что модуль также может экспортировать функцию onEnter. В которую при переходе на соответствующий роут, будут переданы параметры пути и функция store.dispatch. Это позволяет избежать использования componentDidMount для инициализации компонентов. Вместо этого можно выкинуть в store событие (или Promise, если вы, как я, решили выкинуть redux-saga и оставить redux-thunk), обработать его в редюсере, модифицировать state, вызвав тем самым перерисовку компонента.

Подключаем редюсеры к стору
Нам понадобятся DevTools и thunk. Объявим небольшую функцию для инициализации стора.

И еще одну для получения и компоновки всех редюсеров для всех модулей:

Можно сделать менее строго и просто пропускать модули, не содержащие редюсеров, а не падать с исключением, но мне по душе более строгий подход. Если модуль не содержит вообще никакой логики, проще оформить его просто компонентом и добавить в роутер вручную.

Источник

Нянчим проект на React-redux с пелёнок

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.
В начале этого года мы в HeadHunter начали проект, нацеленный на автоматизацию различных HR-процессов у компаний-клиентов. Архитектурой этого проекта на фронте выбрали React-Redux стек.

За 9 месяцев он вырос из небольшого приложения для тестирования сотрудников в многомодульный проект, который сегодня называется “Оценка талантов”. По мере его роста мы сталкивались с вопросами:

Давайте поговорим о том, как мы развивали проект и какие решения принимали. Некоторые из них могут оказаться “холиварными”, а другие, напротив, “классикой” в построении большого проекта на redux. Надеюсь, что описанные ниже практики помогут вам при построении react-redux приложений, а живые примеры помогут разобраться, как работает тот или иной подход.

Общие сведения. Intro.

Веб-приложение представляет собой SPA. Рендеринг приложения только на клиенте (ниже расскажем почему). При разработке мы использовали React-redux стек с различными middlewares, например redux-thunk. В проекте используем es6, компилируемый при сборке через babel. Разработка ведется без применения es7, так как не хотим брать не принятые в стандарт решения.
Частично проект доступен на test.hh.ru, но воспользоваться им можно только компаниям, зарегистрированным на hh.

Структура проекта

В качестве структуры проекта мы изначально взяли разделение приложения на несколько частей. Получилась “классическая” структура для данного стека:

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

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

Здесь нам интересна нижняя часть, которая описывает export модуля. connect — стандартный декоратор react-redux. Он оборачивает наш компонент в дополнительный react компонент, который подписывается на изменения состояния глобального redux стора и передает location поле (плюс оборачивает action creator loadManagers в dispatch стора). Title, composeRestricted — самописные декораторы, которые аналогично оборачивают компонент. Здесь title добавляет заголовки. Второй декоратор — composeRestricted — отвечает за определение прав пользователя, отрисовку restricted страниц, если бекенд отправил соответствующую ошибку: например, нет прав или нет данных. Таких декораторов может быть большое количество: “что-то пошло не так”, дополнительные вычисления и т. д.;

Построение структуры. Детство

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

Все action creators хранились по принципу — одна группа операций, объединенная по смыслу = один файл. Все файлы хранились одним списком в папке.

Выглядело это следующим образом:

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

Что такое “одна группа операций”? Можно сказать, что один файл action creators отвечает за экшены внутри одного редьюсера. В таком случае и action creator и редьюсер имеют одинаковое имя, но такое бывает не в 100% случаев. По смыслу же, это операции над одним типом объектов. Например, employee.js содержит различные методы для работы с определенным объектом — “сотрудник”. Это получение информации о нем, обработка ошибок загрузки данных, изменение данных и добавление нового сотрудника.

Типичный код подобных операций:

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

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

Отрочество

Данная структура прожила ровно до окончания работы над первым модулем. Перед переходом к новому модулю мы посмотрели трезвым взглядом на полученный набор компонентов и решили, что при увеличении системы этот подход к структуре проекта принесет “кашу”, где будет огромное количество файлов на одном уровне системы. Кроме того, если мы решим отдавать на фронт весь js не в одном файле (например, когда размер нашего js-бандла вырастет в мегабайты минифаенной-аглифаенной информации), а в нескольких бандлах, то нам придется довольно долго разруливать все зависимости модулей между собой.

Именно поэтому мы приняли следующее решение (опишу для двух модулей A и B, можно масштабировать на любое количество):

common — включает в себя общие реакт модули. Они представляют собой dummy react component (т. е. компоненты, которые только описывают UI, не управляют им, не влияют\не зависят напрямую от стейта приложения, не вызывают actions) По сути, это реиспользуемые в любом месте приложения компоненты.

blocks – компоненты, которые зависят от общего стейта приложения. Например, блок “уведомления”, или “нотификации”.
moduleA, moduleB — специфичные компоненты для модуля.

И блоки, и модули могут быть smart-компонентами, вызывать какие-то actions и т. д.

С принятыми правилами структура приложения стала выглядеть следующим образом:

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

Таким образом, мы получили четкую структуру, описывающую модульную сущность проекта, в которой можно легко определить отношение js файла к тому или иному модулю, а значит, если мы решим создавать разные бандлы на модули, то нам это не составит никакого труда (достаточно “натравить” вебпак на нужные части).

Разделение стейта приложения на модули

Окей, мы разделили структурно наши файлы, глаз радуется, но что насчет поддержки многомодульной структуры на уровне логики, редьюсеров?

Для небольших приложений описание рутового редьюсера обычно такое:

Для небольших приложений это удобно, так как все данные расположены plain-коллекцией. Но с увеличением размеров приложения, увеличивается количество пересылаемых данных, появляется необходимость в дроблении редьюсеров, разделении их на модули. Как это можно сделать? Прежде чем перейти к результату, рассмотрим две ситуации.

Ситуация первая: “Дробление объекта на отдельные редьюсеры”

Предположим, у нас есть сущность employees. Эта сущность отлично подойдет для разбора различных ситуаций и описания принятия решения. С сущностью “сотрудник” менеджеры компаний могут делать различные действия: загружать, редактировать, создавать, приглашать на тесты, смотреть результаты тестов. Данное поле представляет собой объект с двумя полями: status и data. status определяет текущее состояние поля (FETCH\COMPLETE\FAIL), а data — полезные данные, массив сотрудников. Этого достаточно, чтобы получить данные с сервера и отобразить список сотрудников.
Теперь нам нужно добавить возможность выбора сотрудников:

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

Решить такую задачу можно тремя способами:

Способ первый:

Модифицируем элементы внутри массива employees.data таким образом, что кроме id, имени, фамилии, должности, каждый элемент будет содержать поле selected. При рендере чекбокса смотрим на это поле, а общую сумму считаем, например, так:

Если в будущем нам нужно будет отправить выбранные id, то находим их все через тот же filter. Добавление\удаление выбранных происходит аналогично, через map. Плюсом этого способа является то, что данные хранятся централизованно. Минусом — мы по каждому “чиху” (добавление\снятие флага selected) трогаем весь объект. Это приводит к немалому количеству действий. Логику по работе с выбранными сотрудниками добавляем в employees редьюсер, action creator. Если же хочется разделить эти части (так как работа с выбранными сотрудниками никак не сказывается на основной задаче редьюсера employees — выводить сотрудников и заниматься пагинацией), то стоит посмотреть на два других способа.

Способ второй:

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

Способ третий:

С течением времени, если использовать второй способ каждый раз, мы получим раздувшийся стейт, который будет состоять из employees, selectedEmployees, employee, employeeTest и т. д. Заметим, что редьюсеры связаны друг с другом: selectedEmployees относится к employees, а employeeTest к employee. Поэтому структурируем приложение, создав комбинированный редьюсер. Это даст нам более четкую и удобную структуру:

Достигнуть такой структуры можно, построив иерархию из редьюсеров:

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

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

Ситуация вторая: “Нормализация данных”

При разработке SPA большую роль играет нормализация данных. Продолжим работать с нашими сотрудниками. Мы выбрали нужных сотрудников и отправили их на тест. Затем сотрудник проходит его, и компания получает результаты тестов. Нашему приложению необходимо хранить данные — результаты тестов сотрудников. У одного сотрудника может быть несколько результатов.
Подобную задачу можно также решить несколькими способами.

Плохой способ (человек-оркестр)

Данный способ предлагает доработать структуру employees так, чтобы сохранять полные данные о тесте внутри себя. То есть:

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

Хороший способ

Заметим, что у сотрудника и теста есть id. В базе данных есть связь между тестами и сотрудниками как раз по id сотрудника. Перенимаем такой же подход с бекенда и получаем следующую структуру:

В рутовом редьюсере получим:

Мы структурировали наш стор, разложили все по полочкам, получив четкое понимание функциональности приложения.

Добавляем модули

При добавлении модулей разбиваем представление стейта на common группу и группы, принадлежащие разным модулям:

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

Принципы построения React-компонентов

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

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

На основе этих условий построим модель приложения.

Smart и dumb компоненты

Хорошо описан подход к построению и разделению по данным признакам в следующих ресурсах:

Мы придерживаемся аналогичного подхода. Все common-компоненты являются dummy-компонентами, которые только получают свойства и т. п. Компоненты, относящиеся к тому или иному модулю, могут быть как smart, так и dumb. Мы не вносим жесткого разграничения между ними в структуре, они хранятся рядом.

Чистые компоненты

Практически все наши компоненты не имеют собственного стейта. Такие компоненты либо получают стейт, action creators через декоратор connect, либо с помощью “водопада” от вышестоящих компонентов.

Но есть около 5% компонентов, обладающих собственным стейтом. Что такие компоненты представляют собой? Что хранят в своем стейте? Подобные компоненты в нашем приложении можно поделить на две группы.

Состояние всплывающих элементов:

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

В эту группу попадают компоненты, которые хранят информацию — отображать всплывающий элемент (дропдаун, тултип) или нет.

Типичный код для такого компонента:

Вторая задача внутреннего стейта: временное кеширование или дополнение.

Рассмотрим следующий пример:

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

Нотификации наверху экрана анимируются при появлении и скрытии. Сама сущность нотификации хранится в глобальном стейте приложения, но для процесса скрытия (с последующим удалением) нотификации можно использовать один из нескольких подходов, не загрязняя глобальный стейт такой неважной информацией, как анимация.

Способ первый — кеширование.
Достаточно простой способ. Создаем компонент NotificationsManager, который отвечает за рендеринг компонентов-нотификаций (Notification). После того как NotificationsManager отрендерил очередную нотификацию, он запускает таймер, по окончании которого будет вызван экшен по скрытию нотификации. Перед его вызовом NotificationsManager кеширует нотификацию в своем стейте. Это позволяет удалить саму нотификацию из стора, а закешированные данные внутри локального стейта компонента позволяют провести анимацию ее исчезновения.
Этот способ неудобен тем, что мы обманываем наш стор — он считает, что нотификации никакой нет, но она на самом деле хранится в локальном стейте нашего компонента. Мы хотим “честный” стор, поэтому данный способ нам не подходит.

Способ второй — дополняем информацию из стора локально.
Этот способ более честный и привлекательный, так как не мешает с высокой точностью восстановить данные из стора. Заключается он в том, чтобы NotificationsManager при получении изменений нотификаций со стороны стора добавлял информацию в свой стейт о том, что необходимо делать с нотификацией (анимировать ее появление, исчезновение или ничего не делать). В таком случае NotificationManager уведомляет стор через экшен CLOSE_NOTIFICATION только в момент, когда анимация исчезновения нотификации закончена. Этот подход позволяет отказаться от лишней информации в сторе (статус анимирования нотификации), и в то же время стор остается “единственным источником правды”, с помощью которого можно точно восстановить отображение всего приложения.

Опишем приближенно, каким образом будет работать такой подход. В сторе получаем:

В локальном стейте компонента:

Разберем пример. На уровне всего приложения мы знаем, что у нас существует 3 нотификации. На локальном уровне компонента мы сохраняем информацию, которая не несет смысловой нагрузки для приложения, но важна для локального рендера: 1 исчезает, 3 выезжает, а 2 статична.

Эту информацию можно хранить в сторе, но она не несет никакой полезной смысловой нагрузки. Не нужна никаким компонентам, кроме как самим нотификациям. Даже при восстановлении состояния всего приложения из стора такая информация не нужна и может быть вредна в конкретно этом случае. Кто захочет открыть приложение и увидеть как “что-то резко скрылось”.

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

Роутинг пользователя. Рендеринг

В роутинге пользователя у нас нет ничего сверхестественного. За роутинг отвечает стандартный компонент react-router, с redux мы связываем роутинг через react-router-redux.
Рендеринг нашего приложения происходит только на клиенте.
При первой загрузке страницы сервер отдает нам только стаб, в который для начальной инициализации кладет все данные в формате json, которые нам необходимы для отрисовки страницы: информацию об аккаунте, данные для страницы, описание ошибки (если есть). Затем уже клиентский код рендерит наше приложение.

Данный шаг был сделан по нескольким причинам:

Рассмотрим пример такой работы:

архитектура react redux приложения. image loader. архитектура react redux приложения фото. архитектура react redux приложения-image loader. картинка архитектура react redux приложения. картинка image loader.

Для рендеринга страницы employees нам нужна информация о сотрудниках (employees) и об аккаунте пользователя (account).

Если пользователь вводит в браузере урл этой страницы, то сервер отдаст html-код, содержащий следующий json:

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

Приведем схематический пример такой инициализации:

В реальной жизни поля employees может не оказаться, поэтому мы получаем данные немного по-другому, но суть остается та же.

Следующий случай — на страницу заходят во время работы с приложением. В этом случае страничный контейнер вызовет соответствующий action creator, который пойдет на сервер с acceptType: application/json и получит только необходимые данные (массив сотрудников). Затем отработает редьюсер. “И все будут жить долго и счастливо”.

Неоднозначные решения

Единое место обработки ошибок

В самом начале статьи мы затрагивали код одного из action creator’ов:

После получения данных с сервера мы диспатчим receiveEmployee. Соответственно, далее сотрудник будет сохранен редьюсером в стор, состояние стора изменится, произойдет рендеринг. Эти действия выполняются синхронно. Здесь может скрываться подводный камень — catch отловит следующие категории ошибок:

Мы получили “централизованное” место сбора ошибок. Такое решение нам подходит, так как мы намеренно хотим сообщить пользователю о том, что не получилось загрузить сотрудника, отобразить данные и т. п. Конечному пользователю все равно, где произошла ошибка: на стороне сервера или клиентской логики. В любом случае запрошенные данные он не увидит. Даже если бы мы разграничивали код, например так:

То нам бы пришлось обернуть в try-catch последний dispatch:

и сделать в нем то же самое, что и в предыдущем блок catch:

В итоге мы получили лишнее дублирование кода, не выиграв в конкретно этом случае ничего.

Важно понимать, что при таком подходе нужно разграничивать те случаи, когда централизованная обработка допустима, и те случаи, когда этого стоит избегать.

Динамические редьюсеры

При увеличении количества редьюсеров, разделении их на разные модули и построении иерархической структуры, появляется необходимость избавляться от лишних данных, которые “не используются”. В многомодульном приложении (например, когда у нас 5—10 модулей, в каждом из которых 40—50 редьюсеров) появляется закономерное желание хранить в сторе данные только о выбранном модуле и общие редьюсеры. Зачем так делать? Когда пользователь работает с тем или иным модулем, он с высокой долей вероятности не будет работать с другими модулями. Визуально разные модули отличаются в оформлении, цветовой палитре. Это сделано для того, чтобы пользователь “одним взглядом” понимал, в каком контексте он находится. Соответственно, если пользователь находится в одном модуле, то состояние стора для других модулей, во-первых, не важно, во-вторых, инвалидно (пусто, так как при переходе в другой модуль все данные будут перезагружены с сервера, обновлены). А также, если мы хотим полностью восстановить состояние приложения из стора, нам достаточно содержимого общих полей стора и содержимого полей активного модуля. Остальные модули нам не нужны. Руководствуясь этими принципами, мы решили воспользоваться динамической подменой редьюсеров.

Работает это следующим образом:

Шаг 1.
Создадим редьюсер, описывающий текущий модуль. Такой редьюсер отвечает за информацию, в каком модуле находится пользователь. Этот же редьюсер можем использовать для отрисовки меню (как пример).

Шаг 2.
Все наши модульные редьюсеры находятся внутри отдельного редьюсера. То есть до введения динамического переключения они выглядят следующим образом:

Теперь создаем возможность динамического добавления редьюсеров:

и описываем в файле редьюсеры, которые относятся к модулям:

Шаг 3.
Немного изменяем код конфигурации стора для того, чтобы можно было добавлять динамические редьюсеры:

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

Шаг 4.
Создаем необходимый action creator. С его помощью создаем action для смены выбранного модуля. Например такой:

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

Шаг 5.
Добавляем декоратор, который отвечает за переключение модуля. Он выглядит примерно так:

Соответственно, так как модулей много, этот код легко можно сделать дженерным.

Шаг 6.
Добавляем этот декоратор для наших страничных контейнеров:

Готово! Теперь мы обучили наше приложение изменять набор редьюсеров. Важно, что без острой необходимости данным способом пользоваться нежелательно. Это объясняется тем, что мы “обрубаем” часть стора. Такой подход является оправданным только для больших систем с похожей структурой (модульные, независимые части, объединенные одним проектом).

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *