Redux в Angular с помощью Ngrx. Создание Store в Angular
Update 25 фераля 2020: Бурное развитие JavaScript приводит к изменению базовых принципов управления состояниями в веб приложениях. И связи с этим, данная статья немного устарела, но по прежнему является актуальной. С новой версией можно ознакомиться в статье — Redux в Angular. Управление состояниями в Angular с помощью Ngrx и Nx.
Для Angular есть как минимум 2 реализации Redux
:
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 можно на официальном сайте ngrx
— ngrx/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
.
Исходники
Все исходники находятся на github, в репозитории:
Root State
реализовано в отдельной Nx
библиотеке, которая располагается в libs/store
.
Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий tag — store
.
git checkout store
Предыдущие статьи:
- Статья о настройки Angular Universal & Nx
- Статья о настройке Prettier, tslint и eslint в Angular
- Статья про LocalStorage, SessionStorage в Angular Universal
- Статья про мультиязычность в Angular & Universal с помощью ngx-translate
Следующие статьи: