Table des matières

ngrx

ngrx est une librairie qui permet de gérer l'état de l'application. C'est l'implémentation du pattern redux.

Installation

$ npm install @ngrx/core @ngrx/effects @ngrx/store @ngrx/store-devtools --save

Introduction

Composition de l'état par module

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 {}

Actions

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;

Reducer

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;
    }
  }
}

The Store

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));
  }
}

AppStateest une interface:

import { Company } from './company';
 
export interface AppState {
    companies: { companies: Company[] }
}

on peut créer une interface avec ng g interface models/appState.

Effects

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 => {

Change detection Strategies

@Component({
  selector: 'app-company-list',
  templateUrl: './company-list.component.html',
  styleUrls: ['./company-list.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

Dispatching Actions

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());

Sélecteurs

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().

Sources