Redux в Angular с помощью Ngrx. Создание Store в Angular

Aleksandr Serenko
F.A.F.N.U.R
Published in
5 min readAug 7, 2019
Angular & ngrx & Universal

Update 25 фераля 2020: Бурное развитие JavaScript приводит к изменению базовых принципов управления состояниями в веб приложениях. И связи с этим, данная статья немного устарела, но по прежнему является актуальной. С новой версией можно ознакомиться в статье — Redux в Angular. Управление состояниями в Angular с помощью Ngrx и Nx.

Для Angular есть как минимум 2 реализации Redux:

Ngrx
NGXS

Ngrх является первой реализацией Redux, который реализует основные концепты redux, в react/функциональном стиле. Основные преимущества: огромное количество документации, не считая основного сайта, а также поддержка в Nx.

NGXS же является реализацией Redux в Angular подобном стиле. Основные преимущества: согласуется с Angular Zone, более компактная структура файлов.

Данная статья будет посвящена Ngrx, т.к. нам важна поддержка Nx, да и в принципе, в ngrx есть много привлекательных возможностей, о которых поговорим далее.

Основные понятия

Продолжая развивать проект, в котором уже есть storage и translation, разработка которого описана предыдущих статьях, добавим в него Root Store для app module, а также state для translation, так как если реализовывать обычное приложение Angular, то загрузка переводов, скорее всего будет происходить по http, и соответственно будет промежуток времени, когда в приложении не будет переводов, а только ключи, и поэтому было бы неплохо контролировать состояние загрузки переводов, и только после этого, уже давать работать основному приложению.

В данной статье будем использовать понятия:

Root State — все state’ы для приложения.
State — небольшая, автономная часть Root State, которая может быть загружена лениво.
Понятия Root State и Store эквивалентны, но только надо полагать, что иногда под Store может подразумеваться сама реализация Redux, а не набор State’ов.

Заметим, что в Root State явно будет храниться только state для Router’а, а все остальные state’ы будут загружены лениво (lazy load).

Заметим, что если в процессе разработки понадобились эффекты или события, которые находятся в lazy модулях и вызываемые в Root State Module, но эти модули еще не загружены, то необходимо переписать данные модули так, чтобы вынести из данных ленивых модулей, так называемую“core” логику в подмодуль, и подключить данный подмодуль в Core Module вашего приложения.

Установка

Установим зависимости для ngrx с помощью yarn, а не ng, т.к. Root Store мы вынесем в отдельную библиотеку.

yarn add @ngrx/store @ngrx/effects @ngrx/router-store
yarn add -D @ngrx/schematics @ngrx/store-devtools

Сгенерируем новую библиотеку в NX:

ng g @nrwl/angular:lib store

Добавим Store для root:

ng g @nrwl/angular:ngrx app --module=libs/store/src/lib/root-store.module.ts --root --minimal

Конфигурация Root Store

Сначала создадим папку libs/store/src/lib/+state и добавим туда root.reducer.ts:

import { ActionReducerMap } from '@ngrx/store';
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
import { RouterUrlState } from '../interfaces/router-url-state.interface';/**
* Root state for all application
*
* Notice: Import of modules that always should be in the root store.
* Anthers modules will be loaded with lazy loading.
*/
export interface RootState {
/**
* Router state
*/
router: RouterReducerState<RouterUrlState>;
}
/**
* Our state is composed of a map of action customerReducer functions.
* These customerReducer functions are called with each dispatched action
* and the current or initial state and return a new immutable state.
*/
export const reducers: ActionReducerMap<RootState> = {
router: routerReducer
};
export const rootInitialState: RootState = {
router: null
};

Как можно увидеть, для router state используется — RouterUrlState:

import { Params } from '@angular/router';/**
* Router state URL
*/
export interface RouterUrlState {
/**
* URL
*/
url: string;
/**
* Route params
*/
params: Params;
/**
* Route query params
*/
queryParams: Params;
}

Формально RouterUrlState это интерфейс, который описывает сериализуемые поля из Angular router. Но для того, чтобы Ngrx начал сереализовать в соответствии с описным интерфейсом, реализуем — StoreRouterStateSerializer:

import { Injectable } from '@angular/core';
import { RouterStateSnapshot } from '@angular/router';
import { RouterStateSerializer } from '@ngrx/router-store';
import { RouterUrlState } from '../interfaces/router-url-state.interface';/**
* Custom RouterStateSerializer
*
@see https://ngrx.io/guide/router-store/configuration
*/
@Injectable()
export class StoreRouterStateSerializer implements RouterStateSerializer<RouterUrlState> {
/**
* Only return an object including the URL, params and query params instead of the entire snapshot
*
@param routerState Router state
*/
serialize(routerState: RouterStateSnapshot): RouterUrlState {
let route = routerState.root;
while (route.firstChild) {
route = route.firstChild;
}
const {
url,
root: { queryParams }
} = routerState;
const { params } = route;
return { url, params, queryParams };
}
}

Более подробно с возможностями router store можно на официальном сайте ngrxngrx/router-store.

Опционально: Добавим эффекты для root state:

import { Injectable } from '@angular/core';
import { Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/angular';
import { RootState } from './root.reducer';/**
* Notice: Use root effect's only for root actions (like as router).
* Anthers modules will be loaded with lazy loading.
*/
@Injectable()
export class RootEffects {
constructor(private actions$: Actions, private dataPersistence: DataPersistence<RootState>) {}
}

Теперь добавим описанные reducers и сериализеры в Root Store Module:

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EffectsModule } from '@ngrx/effects';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store';
import { RootEffects } from './+state/root.effects';
import { reducers, rootInitialState } from './+state/root.reducer';
import { StoreRouterStateSerializer } from './services/store-router-state-serializer.service';
@NgModule({
imports: [
RouterModule,
StoreModule.forRoot(reducers, {
initialState: rootInitialState,
metaReducers: [],
runtimeChecks: {
strictActionImmutability: true,
strictStateImmutability: true
}
}),
StoreRouterConnectingModule.forRoot({
serializer: StoreRouterStateSerializer
}),
EffectsModule.forRoot([RootEffects])
]
})
export class RootStoreModule {}

Подключение в проект

Теперь подключим RootStoreModule в приложение.

Для демонстрации, сделаем клон frontend/translation приложения, создание которого было описано здесь, и назовем его frontend/store.

В core module подключим RootStoreModule, а также подключим NxModule

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { NxModule } from '@nrwl/angular';
import { RootStoreModule } from '@medium-stories/store';import { coreContainers, coreRoutes } from './core.common';@NgModule({
imports: [
CommonModule,
NxModule.forRoot(),
RouterModule.forRoot(coreRoutes, { initialNavigation: 'enabled' }),
RootStoreModule,
TranslateModule
],
declarations: [...coreContainers]
})
export class CoreModule {}

Для локального тестирования, добавим в AppBrowserModule модуль StoreDevtoolsModule для лога action’ов

imports: [
...
!environment.production ? StoreDevtoolsModule.instrument({ logOnly: environment.production }) : []
]

Теперь запустим и посмотрим, что есть в store.

Angular & Ngrx

Исходники

Все исходники находятся на github, в репозитории:

Root State реализовано в отдельной Nx библиотеке, которая располагается в libs/store.

Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий tag — store.

git checkout store

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

  1. Статья о настройки Angular Universal & Nx
  2. Статья о настройке Prettier, tslint и eslint в Angular
  3. Статья про LocalStorage, SessionStorage в Angular Universal
  4. Статья про мультиязычность в Angular & Universal с помощью ngx-translate

Следующие статьи:

  1. Статья про Организацию Stat’ов в Angular c Ngrx и Nx

--

--

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

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