Redux в Angular. Управление состояниями в Angular с помощью Ngrx и Nx.

Aleksandr Serenko
F.A.F.N.U.R
Published in
8 min readFeb 25, 2020

В данной статье поговорим о реализации 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 state managment lifecycle

Основные понятия 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

Предыдущие статьи:

  1. Создание переменных в шаблонах Angular. Превращение реактивных свойств в простые объекты.
  2. Angular 9, Universal и Nx. Новые правила сборки SSR приложения.
  3. Кроссплатформенные web storage в Angular 9. Реализация LocalStorage, SessionStorage и Cookies в Angular Universal.
  4. Мультиязычность ngx-translate в Angular 9 c монорепозиторием Nx.
  5. Разбиение локализации ngx-translate на несколько файлов в Nx
  6. Бесконечный скролл в Angular 9 с помощью Intersection Observer API

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock