ngrx est une librairie qui permet de gérer l'état de l'application. C'est l'implémentation du pattern redux.
$ npm install @ngrx/core @ngrx/effects @ngrx/store @ngrx/store-devtools --save
Dans AppModule, on utilise StoreModule.forRoot()
:
import { StoreModule } from '@ngrx/store'; @NgModule({ imports: [ BrowserModule, RouterModule.forRoot(appRoutes), ... StoreModule.forRoot(reducer), ], declarations: [...], bootstrap: [ AppComponent ] }); export class AppModule {}
Dans un module feature, on utilise StoreModule.forFeature()
:
import { StoreModule } from '@ngrx/store'; @NgModule({ imports: [ SharedModule, RouterModule.forChild(productRoutes), ... StoreModule.forFeature('products', reducer), ], declarations: [...], providers: [ ... ] }); export class ProductModule {}
Fichier nommé avec suffixe .actions.ts
, par exemple company.actions.ts
.
import { Company } from '../models/company'; export const LOAD_COMPANIES = 'LOAD_COMPANIES'; export const LOAD_COMPANIES_SUCCESS = 'LOAD_COMPANIES_SUCCESS'; export class LoadCompaniesAction { readonly type = LOAD_COMPANIES; constructor() {} } export class LoadCompaniesSuccessAction { readonly type = LOAD_COMPANIES_SUCCESS; constructor(public payload Company[]) {} } export type Action = LoadCompaniesAction | LoadCompaniesSuccessAction;
Fichier avec le suffixe .reducer.ts
, par exemple company.reducer.ts
.
import * as companyActions from '../actions/company.actions'; export function companyReducer(state = [], action: companyActions.Action) { switch (action.type) { case companyActions.LOAD_COMPANIES_SUCCESS: { return action.payload; } default: { return state; } } }
Dans app.module.ts
:
import { StoreModule } from '@ngrx/store'; import { companyReducer } from './reducers/company.reducer'; // ... imports: [ // ... StoreModule.forRoot({ companies: companyReducer }); ]
Dans le component:
import { Component, OnInit } from '@angular/core'; import { CompanyService } from '../company.service'; import { Observable } from 'rxjs/Observable'; import { Store } from '@ngrx/store'; import { Company } from '../../models'; import { AppState } from 'app/models/appState'; import * as companyAcitons from './../../actions/company.actions'; @Component({ selector: 'app-company-list', templateUrl: './company-list.component.html', styleUrls: ['./company-list.component.scss'], }) export class CompanyListComponent implements OnInit { companies$: Observable<Company[]>; constructor(private store: Store<AppState>) { } ngOnInit() { this.loadCompanies(); this.companies$ = this.store.select(state => state.companies.companies); } loadCompanies() { this.store.dispatch(new companyAcitons.LoadCompaniesAction()); } deleteCompany(companyId: number) { this.store.dispatch(new companyAcitons.DeleteCompanyAction(companyId)); } }
AppState
est une interface:
import { Company } from './company'; export interface AppState { companies: { companies: Company[] } }
on peut créer une interface avec ng g interface models/appState
.
Fichier company.effects.ts
:
import { Injectable } from '@angular/core'; import { Actions, Effect, toPayload } from '@ngrx/effects'; import 'rxjs/add/operator/switchMap'; import { CompanyService } from '../company/company.service'; import * as companyActions from './../actions/company.actions'; import { DeleteCompanySuccessAction } from '../actions/company.actions'; @Injectable() export class CompanyEffects { constructor( private actions$: Actions, private companyService: CompanyService ) { } @Effect() loadCompanies$ = this.actions$ .ofType(companyActions.LOAD_COMPANIES) .switchMap(() => { return this.companyService.loadCompanies() .map(companies => new companyActions.LoadCompaniesSuccessAction(companies)); }); @Effect() deleteCompany$ = this.actions$ .ofType(companyActions.DELETE_COMPANY) .switchMap((action: companyActions.DeleteCompanyAction) => { return this.companyService.deleteCompany(action.payload) .map(company => new companyActions.DeleteCompanySuccessAction(company.id)); }); };
@Effect({ dispatch: false })
Lancer une seule requête lors de plusieurs appels d'action FETCH
:
@Effect() request$: Observable<Action> = this.actions$.pipe( ofType<Request>(RequestTypes.REQUEST_FETCH), distinct( () => RequestTypes.REQUEST_FETCH, this.actions$.pipe( ofType<Request>( RequestTypes.REQUEST_SUCCESS, RequestTypes.REQUEST_FAILED ) ) ), mergeMap(action => this.someService.request(action.data).pipe( map(data => {
@Component({ selector: 'app-company-list', templateUrl: './company-list.component.html', styleUrls: ['./company-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush })
Le lancement d'actions se fait avec le store. Dans le constructeur d'un composant ou d'un service, on injecte le Store:
constructor(private store: Store<IProduct[]>) {}
Par la suite, on peut lancer des actions:
this.store.dispatch(new ProductsFetch());
L'exemple ci-dessous montre comment créer un sélecteur et aussi comment faire de la composition de sélecteurs.
interface IRequestState { pending: boolean; success: boolean; error: any; } export const someRequestSelector: MemoizedSelector<any, IHttpState> = createSelector( AppSelectors.appSelector(), (state: any) => state && state.someRequest ); export const requestSuccess: MemoizedSelector< IHttpState, boolean > = createSelector( someRequestSelector, (request: IRequestState) => request && request.success );
Le sélecteur someRequestSelector
est repris dans le sélecteur requestSuccess
pour éviter de reprendre tout l'état à partir de AppSelectors.appSelector()
.