
À ce jour, pas une seule grande application 
SPA n'est complète sans 
gestion d'état . Il existe plusieurs solutions pour 
Angular dans ce domaine. Le plus populaire d'entre eux est 
NgRx . Il implémente un modèle 
Redux à l'aide de la bibliothèque 
RxJs et dispose de bons outils.
Dans cet article, nous allons brièvement parcourir les principaux modules 
NgRx et nous concentrer plus en détail sur la bibliothèque 
angular-ngrx-data , qui vous permet de créer un 
CRUD complet avec 
gestion d'état en cinq minutes.
Examen NgRx
Vous pouvez en savoir plus sur 
NgRx dans les articles suivants:
- 
Applications réactives sur Angular / NGRX. Partie 1. Introduction- 
Applications réactives sur Angular / NGRX. Partie 2. Magasin- 
Applications réactives sur Angular / NGRX. Partie 3. EffetsExaminez brièvement les principaux modules de 
NgRx , ses avantages et ses inconvénients.
NgRx / store - implémente un modèle Redux.
Implémentation simple en magasincounter.actions.ts
export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const RESET = 'RESET'; 
counter.reducer.ts
 import { Action } from '@ngrx/store'; const initialState = 0; export function counterReducer(state: number = initialState, action: Action) { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } } 
.
Connexion au module
 import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [StoreModule.forRoot({ count: counterReducer })], }) export class AppModule {} 
Utilisation dans le composant
 import { Component } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Observable } from 'rxjs'; import { INCREMENT, DECREMENT, RESET } from './counter'; interface AppState { count: number; } @Component({ selector: 'app-my-counter', template: ` <button (click)="increment()">Increment</button> <div>Current Count: {{ count$ | async }}</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button> `, }) export class MyCounterComponent { count$: Observable<number>; constructor(private store: Store<AppState>) { this.count$ = store.pipe(select('count')); } increment() { this.store.dispatch({ type: INCREMENT }); } decrement() { this.store.dispatch({ type: DECREMENT }); } reset() { this.store.dispatch({ type: RESET }); } } 
 NgRx / store-devtools - vous permet de suivre les changements dans l'application via 
redux-devtools .
Exemple de connexion import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ StoreModule.forRoot(reducers), //      StoreModule StoreDevtoolsModule.instrument({ maxAge: 25, //   25  }), ], }) export class AppModule {} 
 NgRx / effects - vous permet d'ajouter des données entrant dans l'application au référentiel, telles que les requêtes http.
Exemple./effects/auth.effects.ts
 import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Action } from '@ngrx/store'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Observable, of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; @Injectable() export class AuthEffects { // Listen for the 'LOGIN' action @Effect() login$: Observable<Action> = this.actions$.pipe( ofType('LOGIN'), mergeMap(action => this.http.post('/auth', action.payload).pipe( // If successful, dispatch success action with result map(data => ({ type: 'LOGIN_SUCCESS', payload: data })), // If request fails, dispatch failed action catchError(() => of({ type: 'LOGIN_FAILED' })) ) ) ); constructor(private http: HttpClient, private actions$: Actions) {} } 
Connexion de l'effet au module
 import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from './effects/auth.effects'; @NgModule({ imports: [EffectsModule.forRoot([AuthEffects])], }) export class AppModule {} 
 NgRx / entity - offre la possibilité de travailler avec des tableaux de données.
Exempleuser.model.ts
 export interface User { id: string; name: string; } 
user.actions.ts
 import { Action } from '@ngrx/store'; import { Update } from '@ngrx/entity'; import { User } from './user.model'; export enum UserActionTypes { LOAD_USERS = '[User] Load Users', ADD_USER = '[User] Add User', UPSERT_USER = '[User] Upsert User', ADD_USERS = '[User] Add Users', UPSERT_USERS = '[User] Upsert Users', UPDATE_USER = '[User] Update User', UPDATE_USERS = '[User] Update Users', DELETE_USER = '[User] Delete User', DELETE_USERS = '[User] Delete Users', CLEAR_USERS = '[User] Clear Users', } export class LoadUsers implements Action { readonly type = UserActionTypes.LOAD_USERS; constructor(public payload: { users: User[] }) {} } export class AddUser implements Action { readonly type = UserActionTypes.ADD_USER; constructor(public payload: { user: User }) {} } export class UpsertUser implements Action { readonly type = UserActionTypes.UPSERT_USER; constructor(public payload: { user: User }) {} } export class AddUsers implements Action { readonly type = UserActionTypes.ADD_USERS; constructor(public payload: { users: User[] }) {} } export class UpsertUsers implements Action { readonly type = UserActionTypes.UPSERT_USERS; constructor(public payload: { users: User[] }) {} } export class UpdateUser implements Action { readonly type = UserActionTypes.UPDATE_USER; constructor(public payload: { user: Update<User> }) {} } export class UpdateUsers implements Action { readonly type = UserActionTypes.UPDATE_USERS; constructor(public payload: { users: Update<User>[] }) {} } export class DeleteUser implements Action { readonly type = UserActionTypes.DELETE_USER; constructor(public payload: { id: string }) {} } export class DeleteUsers implements Action { readonly type = UserActionTypes.DELETE_USERS; constructor(public payload: { ids: string[] }) {} } export class ClearUsers implements Action { readonly type = UserActionTypes.CLEAR_USERS; } export type UserActionsUnion = | LoadUsers | AddUser | UpsertUser | AddUsers | UpsertUsers | UpdateUser | UpdateUsers | DeleteUser | DeleteUsers | ClearUsers; 
user.reducer.ts
 import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { User } from './user.model'; import { UserActionsUnion, UserActionTypes } from './user.actions'; export interface State extends EntityState<User> { // additional entities state properties selectedUserId: number | null; } export const adapter: EntityAdapter<User> = createEntityAdapter<User>(); export const initialState: State = adapter.getInitialState({ // additional entity state properties selectedUserId: null, }); export function reducer(state = initialState, action: UserActionsUnion): State { switch (action.type) { case UserActionTypes.ADD_USER: { return adapter.addOne(action.payload.user, state); } case UserActionTypes.UPSERT_USER: { return adapter.upsertOne(action.payload.user, state); } case UserActionTypes.ADD_USERS: { return adapter.addMany(action.payload.users, state); } case UserActionTypes.UPSERT_USERS: { return adapter.upsertMany(action.payload.users, state); } case UserActionTypes.UPDATE_USER: { return adapter.updateOne(action.payload.user, state); } case UserActionTypes.UPDATE_USERS: { return adapter.updateMany(action.payload.users, state); } case UserActionTypes.DELETE_USER: { return adapter.removeOne(action.payload.id, state); } case UserActionTypes.DELETE_USERS: { return adapter.removeMany(action.payload.ids, state); } case UserActionTypes.LOAD_USERS: { return adapter.addAll(action.payload.users, state); } case UserActionTypes.CLEAR_USERS: { return adapter.removeAll({ ...state, selectedUserId: null }); } default: { return state; } } } export const getSelectedUserId = (state: State) => state.selectedUserId; // get the selectors const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); // select the array of user ids export const selectUserIds = selectIds; // select the dictionary of user entities export const selectUserEntities = selectEntities; // select the array of users export const selectAllUsers = selectAll; // select the total user count export const selectUserTotal = selectTotal; 
réducteurs / index.ts
 import { createSelector, createFeatureSelector, ActionReducerMap, } from '@ngrx/store'; import * as fromUser from './user.reducer'; export interface State { users: fromUser.State; } export const reducers: ActionReducerMap<State> = { users: fromUser.reducer, }; export const selectUserState = createFeatureSelector<fromUser.State>('users'); export const selectUserIds = createSelector( selectUserState, fromUser.selectUserIds ); export const selectUserEntities = createSelector( selectUserState, fromUser.selectUserEntities ); export const selectAllUsers = createSelector( selectUserState, fromUser.selectAllUsers ); export const selectUserTotal = createSelector( selectUserState, fromUser.selectUserTotal ); export const selectCurrentUserId = createSelector( selectUserState, fromUser.getSelectedUserId ); export const selectCurrentUser = createSelector( selectUserEntities, selectCurrentUserId, (userEntities, userId) => userEntities[userId] ); 
 Quel est le résultat?
Nous obtenons une 
gestion complète de l' 
État avec de nombreux avantages:
- une seule source de données pour l'application,
- l'état est stocké séparément de l'application,
- un style d'écriture unique pour tous les développeurs du projet,
- 
changeDetectionStrategy.OnPush dans tous les composants de l'application,
- débogage pratique via 
redux-devtools ,
- facilité de test, comme 
les réducteurs sont de simples fonctions.
Mais il y a aussi des inconvénients:- un grand nombre de modules apparemment incompréhensibles,
- beaucoup du même type de code que vous ne regarderez pas sans tristesse,
- difficulté à maîtriser en raison de tout ce qui précède.
CRUD
En règle générale, une partie importante de l'application est occupée par le travail avec des objets (création, lecture, mise à jour, suppression), par conséquent, pour la commodité du travail, le concept 
CRUD (Créer, Lire, Mettre à jour, Supprimer) a été inventé. Ainsi, les opérations de base pour travailler avec tous les types d'objets sont standardisées. Il est en plein essor depuis longtemps sur le backend. De nombreuses bibliothèques aident à implémenter cette fonctionnalité et à se débarrasser du travail de routine.
Dans 
NgRx , le module d' 
entité est responsable de 
CRUD , et si vous regardez un exemple de sa mise en œuvre, vous pouvez immédiatement voir qu'il s'agit de la partie la plus grande et la plus complexe de 
NgRx . C'est pourquoi 
John Papa et 
Ward Bell ont créé des données 
angular-ngrx .
angular-ngrx-data
angular-ngrx-data est une 
bibliothèque de compléments 
NgRx qui vous permet de travailler avec des tableaux de données sans écrire de code supplémentaire.
En plus de créer une 
gestion d'état à part entière, elle entreprend la création de services avec 
http pour interagir avec le serveur.
Prenons un exemple
L'installation npm install --save @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools ngrx-data 
Module de données angulaire-ngrx import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EntityMetadataMap, NgrxDataModule, DefaultDataServiceConfig } from 'ngrx-data'; const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' }; export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; export const pluralNames = { Hero: 'heroes' }; @NgModule({ imports: [ CommonModule, NgrxDataModule.forRoot({ entityMetadata, pluralNames }) ], declarations: [], providers: [ { provide: DefaultDataServiceConfig, useValue: defaultDataServiceConfig } ] }) export class EntityStoreModule {} 
Connectez-vous à l'application @NgModule({ imports: [ BrowserModule, HttpClientModule, StoreModule.forRoot({}), EffectsModule.forRoot([]), EntityStoreModule, StoreDevtoolsModule.instrument({ maxAge: 25, }), ], declarations: [ AppComponent ], providers: [], bootstrap: [AppComponent] }) export class AppModule {} 
Nous venons de recevoir l' API générée pour travailler avec le back-end et l'intégration de l' API avec NgRx , sans écrire un seul effet, réducteur et action et sélecteur.Examinons plus en détail ce qui se passe ici.
La 
constante defaultDataServiceConfig définit la configuration de notre API et se connecte au module des 
fournisseurs . La propriété 
racine indique où aller pour les demandes. S'il n'est pas défini, la valeur par défaut sera "api".
 const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' }; 
La 
constante entityMetadata définit les noms du 
magasin qui sera créé lorsque 
NgrxDataModule.forRoot est connecté.
 export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 
Le chemin vers l'API se compose du chemin de base (dans notre cas «crud») et du nom du magasin.
Par exemple, pour obtenir un utilisateur avec un certain nombre, le chemin serait "crud / user / {userId}".
Pour obtenir une liste complète des utilisateurs, la lettre «s» - «crud / user 
s » est ajoutée par défaut à la fin du nom du magasin.
Si vous avez besoin d'un itinéraire différent pour obtenir la liste complète (par exemple, "héros" et non "héros"), vous pouvez le changer en définissant 
pluralNames et en les connectant à 
NgrxDataModule.forRoot .
 export const pluralNames = { Hero: 'heroes' }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 
Connexion dans le composant
Pour vous connecter au composant, vous devez passer le constructeur 
entityServices au constructeur et utiliser la méthode 
getEntityCollectionService pour sélectionner le service du stockage souhaité
 import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Observable } from 'rxjs'; import { Hero } from '@appModels/hero'; import { EntityServices, EntityCollectionService } from 'ngrx-data'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class HeroesComponent implements OnInit { heroes$: Observable<Hero[]>; heroesService: EntityCollectionService<Hero>; constructor(entityServices: EntityServices) { this.heroesService = entityServices.getEntityCollectionService('Hero'); } ... } 
Pour lier la liste au composant, il suffit de prendre la propriété 
entity $ du service, et pour obtenir les données du serveur, appelez la méthode 
getAll () .
 ngOnInit() { this.heroes$ = this.heroesService.entities$; this.heroesService.getAll(); } 
De plus, en plus des données de base, vous pouvez obtenir:
- 
chargé $ , 
chargement $ - obtenir l'état de chargement des données,
- 
erreurs $ - erreurs lors de l'exécution du service,
- 
count $ - nombre total d'enregistrements dans le référentiel.
Les principales méthodes d'interaction avec le serveur:
- 
getAll () - obtenir la liste complète des données,
- 
getWithQuery (query) - obtenir une liste filtrée à l'aide des paramètres de requête,
- 
getByKey (id) - obtenir un enregistrement par identifiant,
- 
ajouter (entité) - ajouter une nouvelle entité avec une demande de support,
- 
supprimer (entité) - supprimer une entité avec une demande de sauvegarde,
- 
update (entity) - met à jour l'entité avec une demande de sauvegarde.
Méthodes de stockage local:
- 
addManyToCache (entité) - ajout d'un tableau de nouvelles entités au référentiel,
- 
addOneToCache (entité) - ajout d'une nouvelle entité uniquement au référentiel,
- 
removeOneFromCache (id) - supprime une entité du référentiel,
- 
updateOneInCache (entity) - met à jour l'entité dans le référentiel,
- 
upsertOneInCache (entité) - si une entité avec l'ID spécifié existe, elle est mise à jour, sinon, une nouvelle est créée,
- et autres
Exemple d'utilisation des composants import { EntityCollectionService, EntityServices } from 'ngrx-data'; import { Hero } from '../../core'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class HeroesComponent implements OnInit { heroes$: Observable<Hero[]>; heroesService: EntityCollectionService<Hero>; constructor(entityServices: EntityServices) { this.heroesService = entityServices.getEntityCollectionService('Hero'); } ngOnInit() { this.heroes$ = this.heroesService.entities$; this.getHeroes(); } getHeroes() { this.heroesService.getAll(); } addHero(hero: Hero) { this.heroesService.add(hero); } deleteHero(hero: Hero) { this.heroesService.delete(hero.id); } updateHero(hero: Hero) { this.heroesService.update(hero); } } 
Toutes 
les méthodes 
angular-ngrx-data sont divisées en travail local et interaction avec le serveur. Cela vous permet d'utiliser la bibliothèque lors de la manipulation de données à la fois sur le client et sur le serveur.
Journalisation
Pour la journalisation, vous devez injecter 
EntityServices dans un composant ou un service et utiliser les propriétés:
- 
réduitActions $ - pour les actions de journalisation,
- 
entityActionErrors $ - pour les erreurs de journalisation.
 import { Component, OnInit } from '@angular/core'; import { MessageService } from '@appServices/message.service'; import { EntityServices } from 'ngrx-data'; @Component({ selector: 'app-messages', templateUrl: './messages.component.html', styleUrls: ['./messages.component.css'] }) export class MessagesComponent implements OnInit { constructor( public messageService: MessageService, private entityServices: EntityServices ) {} ngOnInit() { this.entityServices.reducedActions$.subscribe(res => { if (res && res.type) { this.messageService.add(res.type); } }); } } 
Déplacement vers le référentiel NgRx principal
Comme annoncé lors 
de la conférence ng-conf 2018 , les données 
angular-ngrx seront bientôt migrées vers le référentiel 
NgRx principal.
Réduire la plaque de chaudière avec la vidéo de discussion NgRx - Brandon Roberts & Mike Ryan
 Les références
Les créateurs de anguar-ngrx-data:- John Papa 
twitter.com/John_Papa- Ward Bell 
twitter.com/wardbellDépôts officiels:- 
NgRx- 
angular-ngrx-dataExemple d'application:- 
avec NgRx sans angular-ngrx-data- 
avec NgRx et angular-ngrx-dataCommunauté angulaire russophone sur Telegram