Redux в Angular. Управление состояниями в Angular с помощью Ngrx и Nx.
В данной статье поговорим о реализации Redux в Angular. Поговорим о Ngrx и его концептах, а также приведём реализацию store для монорепозитория Nx.
В статье — Redux в Angular с помощью Ngrx. Создание Store в Angular, уже были рассмотрены основные концепты и методы использования ngrx в Angular. Однако, с того времени Ngrx и Nx обновили свои подходы к организации stat’ов. Поэтому в данной статье мы актуализируем ранее приведённые подходы, а также поговорим о новых договорённостях, которые помогут упростить поддержку и развитие state’а.
Если вы не знакомы с основами Redux, то прежде чем продолжить чтение, прочтите введение в Redux.
Основные моменты Redux
Redux базируется на паре простых идей:
В проекте есть один объект — store, который хранит в себе все состояния (значения) переменных. Изменить state из вне нельзя. Для внесения новых значений в store, используется специальная функция (dispatch), которая принимает action с payload’ом. Action — специальный уникальный объект, с помощью которого можно определить, какие значения в state нужно изменить. Payload — набор значений, которые будут установлены в state или помогут установить значения в state, который передаётся как аргумент в action’е. Для получения значений из store используется специальная функция select’ор (геттер), которая возвращает требуемое значение из store.
Если взять реализации Redux в различных фреймворках, таких как React, Vue или Angular, то каждая реализация будет поддерживать выше описанный концепт в том или ином виде.
Есть одно главное отличие реализации Redux в Angular:
Так как Angular в качестве реализации реактивности использует по умолчанию RxJS, то соответственно и store является реактивным.
Зачем использовать Redux, если есть DI в Angular
Всегда перед использованием Redux в Angular приложении возникает вопрос — А надо ли использовать Redux? Есть же сервисы и DI. Никто не мешает создать Subject/BehaviorSubject и также подписываться на изменения.
У Redux, по сравнению со стандартным подходом есть как минимум 2 основных преимущества.
Во-первых, Redux берёт на себя ответственность за создание реактивных состояний, и разработчику не нужно следить за подписками и отписками.
Во-вторых, все state в Redux — прозрачны и связаны между собой, а значит каждый state может реагировать на изменения в root state. Если вы будете реализовывать нативно, то тогда вам необходимо будет реализовывать свою систему событий (подписок) на изменения других свойств в других сервисах.
В-третьих, Redux защищает от изменений из вне, которые не могут быть отслежены.
Однозначного ответа на вопрос: “Надо ли использовать Redux” — нет, но если вы планируете реализовывать крупный проект и развивать его, то Redux станет хорошим помощником.
Реализация Redux в Angular
Также в Angular, существует несколько реализаций Redux, где лидерами являются две реализации:
Есть ещё Akita, но это не Redux, а альтернативный подход к управлению состояниями в Angular.
NGRX
Ngrx одна из первых реализаций Redux, которая появилась в Angular. Проект активно развивается и временами выкатывает мажорные изменения, пересматривая те или иные концепты.
Следующая схема, описывает все взаимодействия в store:
Основные понятия Ngrx:
- Store — объект, который предоставляет доступ к state.
- State — нативный объект, который хранит состояния
- Reducer — функция, которая на вход принимает action и возвращает новый state
- Action — объект или класс, который передаётся в store для того, чтобы изменить состояние в state (не является реактивным).
- Selector — метод, который возвращает из state нужное состояние, который являтся реактивным.
- Effect — метод, который может следить за определёнными типами action’ов, и порождать новые action’ы, которые могут быть реактивными (http запросы и т.д.)
Базовый принцип работы store:
- Компонент вызывает у root store метод dispatch и передаёт туда новый action.
- Dispatch передаёт action во все редьюсеры, ожидая что хотя бы один из редьюсеров обработает запрос
- Редьюсер, у которого есть метод, который реагирует на action, изменяет state (mutate) и возвращает новое состояние (state)
- Также в момент вызова dispatch, за всеми action’ами следят Effect’ы, которые как и “мутации” редьюсера реагируют на определённые action’ы, и порождают новые action’ы при необходимости.
Подробнее о Ngrx на официальном сайте — ngrx.io
NGXS
Ngxs — ещё одна реализация Redux.
И если ngrx разделяет синхронные и асинхронные action’ы, то в ngxs благодаря подходу описанию экшенов, позволяет объединить синхронные и асинхронные action’ы. Принцип работы аналогичен выше описанному.
Подробнее о Ngxs на официальном сайте — ngxs.io
Akita
Akita — представляет альтернативу, стандартной модели Redux.
Введя ряд новых понятий (query), которые формально изменяют вид selector’ов, то мы получим Akita.
Конечно, реализация в большей степени использует ООП, но вряд ли это можно назвать сильным преимуществом.
Со стороны это чем-то напоминает GraphQL, но только если GraphQL работает на стороне сервера, а в UI приложении только клиент, то Akita пытается реализовать подобного рода механизм. Нужно ли это? Это оставим на усмотрение читателя.
Подробнее о Akite в статье — Встречайте Akita: Новый паттерн управления состоянием для Angular приложений.
Установка и настройка Ngrx
Для того, чтобы установить ngrx, запустим команду:
yarn add @ngrx/store @ngrx/effects @ngrx/router-store
Если вы не используете Nx или хотите, чтобы при установке создались необходимые преcсеты по умолчанию, тогда используйте команду ng, вместо yarn —
ng add @ngrx/store
Добавим схематики и dev tools:
yarn add -D @ngrx/schematics @ngrx/store-devtools
Отметим, для того, чтобы получить state, который будет прведен ниже, также необходимо установить схематики от Nx, хотя сам Nx или DataPersistence пока использованы не будут.
В проекте может быть только один root store, который подключается в AppModule:
imports: [
...
StoreModule.forRoot(),
...
]
Root state — хранит в себе все состояния stat’ов приложения. Так как в Angular реализована ленивая загрузка модулей (lazy loading), то и вместе с ленивой загрузкой будут и добавляться новые части в store.
Абстрактно, root state (store) это объект, свойствами которого являются state других подключённых модулей, в виде следующей структуры:
{
router: { ... }, // Router state,
user: { ... }, // User state
auth: { ... }, // Authentication state
...
}
При подключении root state, в качестве параметров можно передать необходимые state’ы, которые должны быть загружены при запуске приложения. Обычно в этот список попадает только state от навигации (router state), которых хранит текущий активный последний шаг навигации (NavigationEnd).
Передавать что-то кроме router state считается плохой практикой, так как все state находятся и используются конкретными модулями. Но если нужно использовать state из вне, всегда можно создать “common/core” модуль (например — UserCoreModule), в который переместить логику связанную с управлением состоянием и подключить данный подмодуль в AppModule.
Добавим в AppModule root state:
В список reducer’ов входит только router:
И для того, чтобы не хранить избыточную информацию о навигации, добавим RouterStateSerializer:
Более подробно с возможностями router store можно на официальном сайте — ngrx/router-store.
Запустим проект и посмотрим, что получилось:
Так как статья является частью серии статей про Angular, все примеры можно найти в проекте — https://github.com/Fafnur/medium-stories.
Как можно увидеть, в проекте появился root state, в котором есть только state для навигации.
Создание состояний для модулей или ngrx features
Все пользовательские state’ы подключаются в модулях с помощью StoreModule.feature().
Создадим новый модуль и назовём его users:
ng g module users --project=frontend-redux
Из-за того, что для демонстрации используется монорепозиторий Nx, в команде добавлен параметр project=frontend-redux, который указывает проект, в котором нужно создать модуль.
Сгенерируем новый state:
ng g @nrwl/angular:ngrx user --module=apps/frontend/redux/src/app/users/users.module.ts
Подключим UsersModule в AppModule и посмотрим, что получилось:
Как видно из скриншота, в root state появился новый стейт с именем user.
Структура State
Рассмотрим структуру сгенерированного state’а.
user.reducer.ts
В файле редьюсера, можно увидеть следующее:
- USER_FEATURE_KEY — ключ, c которым будет добавлен user state в root state
- State (лучше UserState) — интерфейс, описывающий стуктуру user state’а.
- initialState — начальное состояние user state.
- userReducer — функция реализующая редьюсер для user state, которая по заданному экшену, изменяет и возвращает новый user state.
user.actions.ts
Как можно увидеть из реализации, для пользователя существует 3 action’а.
- loadUser — экшен, который инициирует загрузку пользователя
- loadUserSuccess — экшен, который сообщает о успешной загрузке пользователя, где одним из параметров экшена, является свойство user, в котором будет сохранено значение “загруженного пользователя”
- loadUserFailure — экшен, который сообщает о ошибке загрузки пользователя, где одно из свойств экшена хранит объект ошибки.
user.selectors.ts
Файл селекторов содержит все селекторы, которые формально обращаются к значениям user state.
Селектор — getUserState, выбирает user state из root state.
Опишем часть приведённых селекторов:
- getUserLoaded — возвращает значение loaded из user state
- getUserError — возвращает объект ошибки загрузки пользователя, если такая имеется
- getSelected — более сложный селектор, который использует другой селектор для выбора и возвращения значения
user.effects.ts
В классе эффектов, приведён пример загрузки пользователя и генерации новых экшенов в результате успеха/неуспеха.
user.facade.ts
Файл фасада, оборачивает прямое использование root store, в удобный сервис для вызова новых экшенов, а также предоставляет доступ к значениям user state с помощью селекторов.
Отметим, что данный boilerplate и является всеобъемлющим, но есть пара нюансов, которые не учитываются, такие как два state’а и конфликт имён и т.д. Более подробнее об этом будет рассказано в следующей статье.
Ленивая загрузка state’ов
Но так как подключать модули на прямую не хорошо, сделаем ленивую загрузку модуля — UsersModule.
Немного отрефакторим AppComponent, добавим HomeModule и HomeComponent и вынесем туда всю вёрстку, а в AppComponent оставим лишь в роли точки входа.
Создадим компонент для отображения страницы пользователя.
ng g component user --project=frontend-redux --skip-import
Обновим UserModule и добавим ленивую загрузку:
Подключим ленивые модули в AppModule:
Запустим проект и посмотрим на ленивую загрузку:
Резюме
В данной статье поговорили об основных концептах Redux и привели несколько разных реализаций в Angular, предоставив краткое описание каждой из технологий.
На примере добавили Ngrx в проект и сгенерировали новый state с помощью схематик Nx. Сделали небольшой обзор сгенерированных файлов, описав их назначение.
Также сгенерировали несколько новых модулей в приложении и показали на примере подключение ngrx features в двух вариантах: обычном и с использованием lazy loading.
Исходники
Все исходники находятся на github, в репозитории:
Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий тег — redux.
git checkout redux
Код можно посмотреть в разделе https://github.com/Fafnur/medium-stories/tree/master/apps/frontend/redux
Ссылки
Подписывайтесь на канал, чтобы не пропустить новые статьи про Angular и новости из мира фронтенд разработки.
Добавляйтесь в группу ВК: https://vk.com/fafnur
Добавляйтесь в группу в Fb: https://www.facebook.com/groups/fafnur/
Телеграм канал: https://t.me/f_a_f_n_u_r
Twitter: https://twitter.com/Fafnur1
Предыдущие статьи:
- Создание переменных в шаблонах Angular. Превращение реактивных свойств в простые объекты.
- Angular 9, Universal и Nx. Новые правила сборки SSR приложения.
- Кроссплатформенные web storage в Angular 9. Реализация LocalStorage, SessionStorage и Cookies в Angular Universal.
- Мультиязычность ngx-translate в Angular 9 c монорепозиторием Nx.
- Разбиение локализации ngx-translate на несколько файлов в Nx
- Бесконечный скролл в Angular 9 с помощью Intersection Observer API