Как стать автором
Обновить

Архитектура приложения React Redux

Время на прочтение5 мин
Количество просмотров31K

Предисловие


Это мой первый пост на Хабре, поэтому не судите сильно строго (ну или судите, но конструктивно).

Хотелось бы отметить, что в этом подходе основным преимуществом для меня стало то, что мы четко разграничиваем и делегируем бизнес логику по модулям. Один модуль отвечает за что-то одно и за что-то весьма конкретное. То есть, при таком подходе, в процессе разработки не возникает мысли: «а где мне лучше (правильнее) будет сделать вот это вот?». При таком подходе сразу ясно, где конкретно должна решаться задача/проблема

Файловая структура


Структура проекта +- стандартная. Попытаюсь рассказать вкратце, что используется из чего состоит непосредственно сборка



  • Для готовки асинхронных экшнов я использую saga middleware — куда более гибкая и сильная вещь, относительно thunk middleware
  • Стилизацией компонентов у меня занимаются css-modules. По сути концепция та же, что и у styled components, но мне как-то оно удобнее
  • Прослойкой между контейнерами и компонентами, которая контролирует прокидываемые из стора пропсы в компонент (фильтрация, мемоизация и возможность удобного манипулирования над именами полей любого куска состояния приложения) — reselect

Redux sagas


Redux sagas documentation

Основная суть саг (да и не только) — мы отделяем бизнес логику работы с запросами по модулям. Каждая сага отвечает за свой кусок АПИ. Если бы мне понадобилось получить данные о юзере и вызвать экшн по их успешному получению — я бы это сделал в отдельном модуле usersSagas.js

Основные факторы (для меня) в пользу саг это:
UPD (добавил пункт про promise-hell)
  • 1) То, что экшны, по-хорошему должны быть просто функциями, отдающими объект. Если часть экшнов будет выглядеть по-другому (а с использованием thunk оно так и происходит), хочется как-то привести их к одному виду, ибо как-то это не укладывается в единую концепцию. Хочется вынести логику запросов и работы с данными для запросов в отдельный модуль — для этого и нужны саги
  • 2) Как правило, если нам требуется сделать несколько запросов, в которых используются данные из ответов на предыдущие запросы (а мы же не хотим хранить промежуточные данные в сторе?), redux-thunk предлагает нам сделать новый экшн, в котором мы будем вызывать другой асинк-экшн запрос, в котором делаются еще 2 (к примеру) такие же итерации. Это попахивает promise-hell и выглядит обычно неаккуратно и в принципе неудобно для использования (слишком много вложенностей), как по мне




css-modules/styled components


Часто вижу на проектах, что люди пишут правила для стилизации компонентов в каких-то общих модулях css.

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





Reselect


Selector’ы преследуют несколько целей:

  • Вынесение логики работы с данными, приходящими в комопнент в отдельный модуль. Это, когда нам нужно получить как бы отфильтрованные данные, но мы не хотим заводить под это кусок стора (это здорово, если мы НЕ хотим заводить отдельный кусок стора под это). До того, как я начал использовать реселект, я делал это внутри компонента. Делал какой-нибудь метод getFilteredItems, в котором возвращал отфильтрованные данные
  • Мемоизация и контроль над пропсами, которые будут контролить ререндер компонента
  • Гибкость рисунка дерева состояния. К примеру у нас приходят какие-то данные с бека по юзеру. Нам в компоненте нужно получить только какой-то кусок этих данных. К примеру это будет массив friends. Мы пишем селектор под друзей и в контейнерах, которые используют этот кусок стейта его импортируем. Если бы мы не использовали селектор, то мы бы ручками писали название этого поля в каждом контейнере. Представим ситуацию, что логика приложения поменялась, и поле friends переименовали на contacts. Без использования reselect мы будем лазить по всем контейнерам, так или иначе использующими это поле, и менять его название. С использованием реселекта — поменяем парочку реселектов.

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



Components and Containers


В классическом подходе к React'у (без использования библиотек для хранения стора, как отдельной сущности) есть два вида компонентов — Presentational и Container-Components. Обычно (как я это понимаю) это не строгое разделение по папкам, а скорее концептуальное разделение.

Presentational — глупые компоненты. Они представляют из себя, по сути, только верстку и отображение данных, которые в них прокинули в качестве пропсов. (пример такого компонента можно глянуть выше в css-modules)

Container-Components — компоненты, инкапсулирующие логику работы с, к примеру, лайф-сайклом компонента. Они отвечают за вызов экшна, отвечающего за запрос на получение данных, к примеру. Возвращают минимум верстки, ибо верстка изолирована в Presentational-Components.

Пример +- Container-Component:



Redux Containers — это, по сути, прослойка между стором редакс и компонентами реакта. В них мы вызываем селекторы и прокидываем экшны в пропсы реакт компонента.

Я ЗА то, чтобы на каждый чих иметь свой контейнер. Во-первых это дает бОльший контроль над пропсами, прокинутыми в компонент, во-вторых — контроль над производительностью, с помощью реселекта.

Часто бывает так, что нам нужно переиспользовать один компонент, но с разными кусками стора. Получается, что для этого нам просто нужно будет написать еще один контейнер и вернуть его там, где необходимо. То есть, связь Многие к одному (многие — контейнеры, один — компонент. По мне — удобно и концептуальненько)

Также хотелось бы привести более частый пример в пользу контейниризации большинства компонентов.

У нас есть массив данных (массив юзеров, к примеру), который мы получаем с некоторой АПИ. Также у нас есть инфинит скролл, от которого заказчик не собирается отказываться. Мы очень долго листали вниз и подгрузили около 10к+ данных. А теперь мы поменяли какое-то свойство одного юзера. Наше приложение будет сильно тормозить, потому что:

  1. Мы прикрутили глобальный контейнер на всю страницу со списком юзеров
  2. При изменении одного поля одного элемента массива users у нас в редьюсере users вернулся НОВЫЙ массив с новыми элементами и индексами
  3. Все компоненты, размещенные в дереве компонента UsersPage будут перерисовываться. В том числе и каждый компонент User (элемент массива)

Как этого избежать?

Мы делаем контейнера на

  • массив с юзерами
  • элемент массива (один юзер)

После этого мы в компоненте, который завернут в контейнер «массив с юзерами» возвращаем контейнер «элемент массива (один юзер)» с прокинутыми туда key (react required prop), index

В контейнере «элемент массива (один юзер)» в mapStateToProps мы вторым аргументом принимаем ownProps компонента, который контейнер возвращает (среди них index). По индексу мы вытаскиваем из стора напрямую только один элемент массива.

Дальше заоптимизировать перерисовку только изменившегося элемента станет гораздо проще (перерисовывается весь массив, потому что в редьюсере мы сделали какой-нибудь map — возвращающий НОВЫЙ массив с новыми индексами под каждый элемент) — тут нам уже поможет reselect

массив container:



элемент container:



element-selector:



Если есть какие-то дополнения — с удовольствием их почитаю в комментах.
Теги:
Хабы:
Всего голосов 6: ↑4 и ↓2+4
Комментарии62

Публикации

Истории

Работа

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань