
Hasta la fecha, ni una sola aplicación de 
SPA grande está completa sin 
la administración del estado . Hay varias soluciones para 
Angular en esta área. El más popular de estos es 
NgRx . Implementa un patrón 
Redux usando la biblioteca 
RxJs y tiene buenas herramientas.
En este artículo, 
repasaremos brevemente los módulos principales de 
NgRx y nos enfocaremos con más detalle en la biblioteca 
angular-ngrx-data , que le permite hacer un 
CRUD completo con 
administración de estado en cinco minutos.
Revisión de NgRx
Puede leer más sobre 
NgRx en los siguientes artículos:
- 
Aplicaciones reactivas en Angular / NGRX. Parte 1. Introducción- 
Aplicaciones reactivas en Angular / NGRX. Parte 2. Almacenar- 
Aplicaciones reactivas en Angular / NGRX. Parte 3. EfectosConsidere brevemente los módulos principales de 
NgRx , sus ventajas y desventajas.
NgRx / store : implementa un patrón Redux.
Implementación simple de la tiendacounter.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; } } 
.
Conexión al módulo.
 import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [StoreModule.forRoot({ count: counterReducer })], }) export class AppModule {} 
Uso en componente
 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 : le permite realizar un seguimiento de los cambios en la aplicación a través de 
redux-devtools .
Ejemplo de conexión import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ StoreModule.forRoot(reducers), //      StoreModule StoreDevtoolsModule.instrument({ maxAge: 25, //   25  }), ], }) export class AppModule {} 
 NgRx / effects : le permite agregar datos que 
ingresan en la aplicación al repositorio, como las solicitudes http.
Ejemplo./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) {} } 
Conectando el efecto al módulo
 import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from './effects/auth.effects'; @NgModule({ imports: [EffectsModule.forRoot([AuthEffects])], }) export class AppModule {} 
 NgRx / entity : proporciona la capacidad de trabajar con matrices de datos.
Ejemplouser.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; 
reductores / 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] ); 
 Cual es el resultado?
Obtenemos una 
gestión estatal completa con muchas ventajas:
- una única fuente de datos para la aplicación,
- el estado se almacena por separado de la aplicación,
- un estilo de escritura único para todos los desarrolladores del proyecto,
- 
changeDetectionStrategy.OnPush en todos los componentes de la aplicación,
- depuración conveniente a través de 
redux-devtools ,
- facilidad de prueba, como 
Los reductores son funciones puras.
Pero también hay desventajas:- una gran cantidad de módulos aparentemente incomprensibles,
- mucho del mismo tipo de código que no mirarás sin tristeza,
- Dificultad para dominar debido a todo lo anterior.
CRUDO
Como regla general, una parte importante de la aplicación está ocupada trabajando con objetos (creación, lectura, actualización, eliminación), por lo tanto, para la comodidad del trabajo, se creó el concepto 
CRUD (Crear, Leer, Actualizar, Eliminar). Por lo tanto, las operaciones básicas para trabajar con todo tipo de objetos están estandarizadas. Ha estado en auge durante mucho tiempo en el backend. Muchas bibliotecas ayudan a implementar esta funcionalidad y deshacerse del trabajo de rutina.
En 
NgRx , el módulo de 
entidad es responsable de 
CRUD , y si observa un ejemplo de su implementación, puede ver de inmediato que esta es la parte más grande y compleja de 
NgRx . Es por eso que 
John Papa y 
Ward Bell crearon 
angular-ngrx-data .
angular-ngrx-data
angular-ngrx-data es una 
biblioteca de complementos 
NgRx que le permite trabajar con matrices de datos sin escribir código adicional.
Además de crear una 
gestión estatal completa, se compromete a crear servicios con 
http para interactuar con el servidor.
Considera un ejemplo
Instalación npm install --save @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools ngrx-data 
Módulo de datos angulares-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 {} 
Conéctate a la aplicación @NgModule({ imports: [ BrowserModule, HttpClientModule, StoreModule.forRoot({}), EffectsModule.forRoot([]), EntityStoreModule, StoreDevtoolsModule.instrument({ maxAge: 25, }), ], declarations: [ AppComponent ], providers: [], bootstrap: [AppComponent] }) export class AppModule {} 
Acabamos de obtener la API generada para trabajar con el back-end y la integración de la API con NgRx , sin escribir un solo efecto, reductor y acción y selector.Examinemos con más detalle lo que está sucediendo aquí.
La 
constante defaultDataServiceConfig establece la configuración de nuestra API y se conecta al módulo de 
proveedores . La propiedad 
raíz indica a dónde ir para las solicitudes. Si no está configurado, el valor predeterminado será "api".
 const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' }; 
La 
constante entityMetadata define los nombres de la 
tienda que se crearán cuando 
se conecte 
NgrxDataModule.forRoot .
 export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 
La ruta a la API consiste en la ruta base (en nuestro caso, "crud") y el nombre de la tienda.
Por ejemplo, para obtener un usuario con un número determinado, la ruta sería "crud / user / {userId}".
Para obtener una lista completa de usuarios, la letra "s" - "crud / user 
s " se agrega al final del nombre de la tienda de forma predeterminada.
Si necesita una ruta diferente para obtener la lista completa (por ejemplo, "héroes" y no "héroes"), puede cambiarla configurando 
pluralNames y conectándolos a 
NgrxDataModule.forRoot .
 export const pluralNames = { Hero: 'heroes' }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 
Conexión en componente
Para conectarse en el componente, debe pasar el constructor 
entityServices al constructor y usar el método 
getEntityCollectionService para seleccionar el servicio del almacenamiento deseado
 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'); } ... } 
Para enlazar la lista al componente, es suficiente tomar las 
entidades $ property del servicio y obtener los datos del servidor, llame al método 
getAll () .
 ngOnInit() { this.heroes$ = this.heroesService.entities$; this.heroesService.getAll(); } 
Además de los datos básicos, también puede obtener:
- 
cargado $ , 
cargando $ - obteniendo el estado de carga de datos,
- 
errores $ - errores cuando el servicio se está ejecutando,
- 
cuenta $ - número total de registros en el repositorio.
Los principales métodos para interactuar con el servidor:
- 
getAll () - obteniendo la lista completa de datos,
- 
getWithQuery (consulta) : filtrar una lista utilizando parámetros de consulta,
- 
getByKey (id) - obteniendo un registro por identificador,
- 
add (entity) : agrega una nueva entidad con una solicitud de respaldo,
- 
eliminar (entidad) : eliminar una entidad con una solicitud de respaldo,
- 
actualizar (entidad) : actualiza la entidad con una solicitud de respaldo.
Métodos de almacenamiento local:
- 
addManyToCache (entidad) : agrega una matriz de nuevas entidades al repositorio,
- 
addOneToCache (entidad) : agrega una nueva entidad solo a la tienda,
- 
removeOneFromCache (id) : elimina una entidad del repositorio,
- 
updateOneInCache (entidad) : actualiza la entidad en el repositorio,
- 
upsertOneInCache (entidad) : si existe una entidad con el ID especificado, se actualiza; de lo contrario, se crea una nueva,
- y otros
Ejemplo de uso de componentes 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); } } 
Todos 
los métodos de 
angular-ngrx-data se dividen en trabajar localmente e interactuar con el servidor. Esto le permite utilizar la biblioteca al manipular datos tanto en el cliente como en el servidor.
Registro
Para iniciar sesión, debe inyectar 
EntityServices en un componente o servicio y usar las propiedades:
- 
reduceActions $ - para acciones de registro,
- 
entityActionErrors $ - para errores de registro.
 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); } }); } } 
Pasar al repositorio principal de NgRx
Como se anunció en 
ng-conf 2018 , 
angular-ngrx-data se 
migrará al repositorio principal de 
NgRx pronto .
Reduciendo el Boilerplate con el video de NgRx talk
 Referencias
Los creadores de anguar-ngrx-data:- John Papa 
twitter.com/John_Papa- Ward Bell 
twitter.com/wardbellRepositorios oficiales:- 
NgRx- 
angular-ngrx-dataEjemplo de aplicación:- 
con NgRx sin angular-ngrx-data- 
con NgRx y angular-ngrx-dataComunidad angular de habla rusa en Telegram