Tutorial de Ngrx y Angular 2: creación de una aplicación reactiva
Publicado: 2022-03-11Hablamos mucho sobre la programación reactiva en el ámbito Angular. La programación reactiva y Angular 2 parecen ir de la mano. Sin embargo, para cualquiera que no esté familiarizado con ambas tecnologías, puede ser una tarea bastante abrumadora descubrir de qué se trata.
En este artículo, a través de la creación de una aplicación Angular 2 reactiva con Ngrx, aprenderá qué es el patrón, dónde puede resultar útil y cómo se puede usar para crear mejores aplicaciones Angular 2.
Ngrx es un grupo de bibliotecas Angular para extensiones reactivas. Ngrx/Store implementa el patrón Redux utilizando los conocidos observables RxJS de Angular 2. Brinda varias ventajas al simplificar el estado de su aplicación a objetos simples, hacer cumplir el flujo de datos unidireccional y más. La biblioteca Ngrx/Effects permite que la aplicación se comunique con el mundo exterior activando efectos secundarios.
¿Qué es la programación reactiva?
La programación reactiva es un término que escuchas mucho en estos días, pero ¿qué significa realmente?
La programación reactiva es una forma en que las aplicaciones manejan los eventos y el flujo de datos en sus aplicaciones. En la programación reactiva, diseñas tus componentes y otras piezas de tu software para reaccionar a esos cambios en lugar de pedir cambios. Esto puede ser un gran cambio.
Una gran herramienta para la programación reactiva, como sabrás, es RxJS.
Al proporcionar elementos observables y una gran cantidad de operadores para transformar los datos entrantes, esta biblioteca lo ayudará a manejar eventos en su aplicación. De hecho, con los observables, puede ver el evento como un flujo de eventos y no como un evento único. Esto te permite combinarlos, por ejemplo, para crear un nuevo evento que escucharás.
La programación reactiva es un cambio en la forma en que se comunica entre las diferentes partes de una aplicación. En lugar de enviar datos directamente al componente o servicio que los necesitaba, en la programación reactiva, es el componente o servicio el que reacciona a los cambios de datos.
Una palabra sobre Ngrx
Para comprender la aplicación que creará a través de este tutorial, debe sumergirse rápidamente en los conceptos básicos de Redux.
Tienda
La tienda puede verse como su base de datos del lado del cliente pero, lo que es más importante, refleja el estado de su aplicación. Puedes verlo como la única fuente de verdad.
Es lo único que alteras cuando sigues el patrón Redux y lo modificas enviándole acciones.
reductor
Los reductores son las funciones que saben qué hacer con una acción determinada y el estado anterior de su aplicación.
Los reductores tomarán el estado anterior de su tienda y le aplicarán una función pura. Puro significa que la función siempre devuelve el mismo valor para la misma entrada y que no tiene efectos secundarios. Del resultado de esa función pura, tendrás un nuevo estado que se pondrá en tu tienda.
Comportamiento
Las acciones son la carga útil que contiene la información necesaria para modificar su tienda. Básicamente, una acción tiene un tipo y una carga útil que su función de reducción tomará para alterar el estado.
Despachador
Los despachadores son simplemente un punto de entrada para que envíe su acción. En Ngrx, hay un método de envío directamente en la tienda.
software intermedio
El middleware son algunas funciones que interceptarán cada acción que se envíe para crear efectos secundarios, aunque no las usará en este artículo. Se implementan en la biblioteca Ngrx/Effect, y existe una gran posibilidad de que los necesite mientras crea aplicaciones del mundo real.
¿Por qué usar Ngrx?
Complejidad
El almacenamiento y el flujo de datos unidireccional reducen en gran medida el acoplamiento entre las partes de su aplicación. Este acoplamiento reducido reduce la complejidad de su aplicación, ya que cada parte solo se preocupa por estados específicos.
Estampación
Todo el estado de su aplicación se almacena en un solo lugar, por lo que es fácil tener una vista global del estado de su aplicación y ayuda durante el desarrollo. Además, con Redux vienen muchas buenas herramientas de desarrollo que aprovechan la tienda y pueden ayudar a reproducir un cierto estado de la aplicación o hacer viajes en el tiempo, por ejemplo.
Simplicidad arquitectónica
Muchos de los beneficios de Ngrx se pueden lograr con otras soluciones; después de todo, Redux es un patrón arquitectónico. Pero cuando tiene que crear una aplicación que se ajuste perfectamente al patrón de Redux, como las herramientas de edición colaborativa, puede agregar funciones fácilmente siguiendo el patrón.
Aunque no tiene que pensar en lo que está haciendo, agregar algunas cosas como análisis a través de todas sus aplicaciones se vuelve trivial ya que puede rastrear todas las acciones que se envían.
Pequeña curva de aprendizaje
Dado que este patrón es tan simple y ampliamente adoptado, es realmente fácil para las personas nuevas en su equipo ponerse al día rápidamente con lo que hizo.
Ngrx brilla más cuando tiene muchos actores externos que pueden modificar su aplicación, como un panel de monitoreo. En esos casos, es difícil administrar todos los datos entrantes que se envían a su aplicación y la administración del estado se vuelve difícil. Es por eso que desea simplificarlo con un estado inmutable, y esto es algo que nos brinda la tienda Ngrx.
Construyendo una aplicación con Ngrx
El poder de Ngrx brilla más cuando tiene datos externos que se envían a nuestra aplicación en tiempo real. Con eso en mente, construyamos una cuadrícula simple de freelancers que muestre a los freelancers en línea y te permita filtrarlos.
Configuración del proyecto
Angular CLI es una herramienta increíble que simplifica enormemente el proceso de configuración. Es posible que desee no usarlo, pero tenga en cuenta que el resto de este artículo lo usará.
npm install -g @angular/cliA continuación, desea crear una nueva aplicación e instalar todas las bibliotecas de Ngrx:
ng new toptal-freelancers npm install ngrx --saveReductor de autónomos
Los reductores son una pieza central de la arquitectura Redux, entonces, ¿por qué no comenzar con ellos primero mientras se crea la aplicación?
Primero, crea un reductor “freelancers” que se encargará de crear nuestro nuevo estado cada vez que se envíe una acción a la tienda.
freelancer-grid/freelancers.reducer.ts
import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, thumbnail: string } export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); default: return state; } }Así que aquí está nuestro reductor de autónomos.
Esta función se llamará cada vez que se envíe una acción a través de la tienda. Si la acción es FREELANCERS_LOADED , creará una nueva matriz a partir de la carga útil de la acción. Si no es así, devolverá la referencia de estado anterior y no se agregará nada.
Es importante señalar aquí que, si se devuelve la referencia de estado anterior, el estado se considerará sin cambios. Esto significa que si llamas a state.push(something) , no se considerará que el estado ha cambiado. Tenga eso en cuenta mientras realiza sus funciones de reducción.
Los estados son inmutables. Se debe devolver un nuevo estado cada vez que cambia.
Componente de cuadrícula de autónomos
Cree un componente de cuadrícula para mostrar a nuestros trabajadores autónomos en línea. Al principio, solo reflejará lo que hay en la tienda.
ng generate component freelancer-gridPonga lo siguiente en freelancer-grid.component.ts
import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component.html', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; constructor(private store: Store<AppState>) { this.freelancers = store.select('freelancers'); } }Y lo siguiente en freelancer-grid.component.html :
<span class="count">Number of freelancers online: {{(freelancers | async).length}}</span> <div class="freelancer fade thumbail" *ngFor="let freelancer of freelancers | async"> <button type="button" class="close" aria-label="Close" (click)="delete(freelancer)"><span aria-hidden="true">×</span></button><br> <img class="img-circle center-block" src="{{freelancer.thumbnail}}" /><br> <div class="info"><span><strong>Name: </strong>{{freelancer.name}}</span> <span><strong>Email: </strong>{{freelancer.email}}</span></div> <a class="btn btn-default">Hire {{freelancer.name}}</a> </div>Entonces, ¿qué acabas de hacer?
Primero, ha creado un nuevo componente llamado freelancer-grid .
El componente contiene una propiedad denominada freelancers que forma parte del estado de la aplicación contenido en la tienda Ngrx. Al usar el operador de selección, elige que solo la propiedad de los freelancers le notifique el estado general de la aplicación. Entonces, cada vez que la propiedad de los freelancers del estado de la aplicación cambie, su observable será notificado.
Una cosa que es hermosa con esta solución es que su componente tiene solo una dependencia, y es la tienda la que hace que su componente sea mucho menos complejo y fácilmente reutilizable.
En la parte de la plantilla, no hiciste nada demasiado complejo. Observe el uso de tubería asíncrona en *ngFor . El observable de freelancers no es directamente iterable, pero gracias a Angular, tenemos las herramientas para desenvolverlo y vincular el dom a su valor mediante el uso de la tubería asíncrona. Esto hace que trabajar con lo observable sea mucho más fácil.
Adición de la funcionalidad de eliminación de autónomos
Ahora que tiene una base funcional, agreguemos algunas acciones a la aplicación.
Desea poder eliminar a un trabajador independiente del estado. Según cómo funciona Redux, primero debe definir esa acción en cada estado que se ve afectado por ella.
En este caso, es sólo el reductor de freelancers :
export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }Aquí es realmente importante crear una nueva matriz a partir de la anterior para tener un nuevo estado inmutable.
Ahora, puede agregar una función de eliminación de trabajadores independientes a su componente que enviará esta acción a la tienda:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }¿No parece sencillo?

Ahora puede eliminar a un trabajador independiente específico del estado y ese cambio se propagará a través de su aplicación.
Ahora, ¿qué sucede si agrega otro componente a la aplicación para ver cómo pueden interactuar entre sí a través de la tienda?
Reductor de filtro
Como siempre, empecemos por el reductor. Para ese componente, es bastante simple. Desea que el reductor siempre devuelva un nuevo estado con solo la propiedad que despachamos. Debería verse así:
import { Action } from '@ngrx/store'; export interface IFilter { name: string, email: string, } export const ACTIONS = { UPDATE_FITLER: 'UPDATE_FITLER', CLEAR_FITLER: 'CLEAR_FITLER', } const initialState = { name: '', email: '' }; export function filterReducer( state: IFilter = initialState, action: Action): IFilter { switch (action.type) { case ACTIONS.UPDATE_FITLER: // Create a new state from payload return Object.assign({}, action.payload); case ACTIONS.CLEAR_FITLER: // Create a new state from initial state return Object.assign({}, initialState); default: return state; } }Componente de filtro
import { Component, OnInit } from '@angular/core'; import { IFilter, ACTIONS as FilterACTIONS } from './filter-reducer'; import { Store } from '@ngrx/store'; import { FormGroup, FormControl } from '@angular/forms'; import * as Rx from 'RxJS'; @Component({ selector: 'app-filter', template: '<form class="filter">'+ '<label>Name</label>'+ '<input type="text" [formControl]="name" name="name"/>'+ '<label>Email</label>'+ '<input type="text" [formControl]="email" name="email"/>'+ '<a (click)="clearFilter()" class="btn btn-default">Clear Filter</a>'+ '</form>', styleUrls: ['./filter.component.scss'], }) export class FilterComponent implements OnInit { public name = new FormControl(); public email = new FormControl(); constructor(private store: Store<any>) { store.select('filter').subscribe((filter: IFilter) => { this.name.setValue(filter.name); this.email.setValue(filter.email); }) Rx.Observable.merge(this.name.valueChanges, this.email.valueChanges).debounceTime(1000).subscribe(() => this.filter()); } ngOnInit() { } filter() { this.store.dispatch({ type: FilterACTIONS.UPDATE_FITLER, payload: { name: this.name.value, email: this.email.value, } }); } clearFilter() { this.store.dispatch({ type: FilterACTIONS.CLEAR_FITLER, }) } }En primer lugar, ha realizado una plantilla sencilla que incluye un formulario con dos campos (nombre y correo electrónico) que refleja nuestro estado.
Mantiene esos campos sincronizados con el estado de manera bastante diferente a lo que hizo con el estado de los freelancers . De hecho, como ha visto, se suscribió al estado del filtro y, cada vez, se activa y asigna el nuevo valor al formControl .
Una cosa buena con Angular 2 es que le brinda muchas herramientas para interactuar con observables.
Ha visto la canalización asíncrona anteriormente, y ahora ve la clase formControl que le permite tener un observable en el valor de una entrada. Esto permite cosas sofisticadas como lo que hizo en el componente de filtro.
Como puede ver, usa Rx.observable.merge para combinar los dos observables proporcionados por sus formControls , y luego elimina ese nuevo observable antes de activar la función de filter .
En palabras más simples, espere un segundo después de que el nombre o el control del formulario de correo formControl hayan cambiado y luego llame a la función de filter .
¿No es maravilloso?
Todo eso se hace en unas pocas líneas de código. Esta es una de las razones por las que te encantará RxJS. Te permite hacer fácilmente muchas de esas cosas sofisticadas que de otro modo habrían sido más complicadas.
Ahora pasemos a esa función de filtro. ¿Qué hace?
Simplemente despacha la acción UPDATE_FILTER con el valor del nombre y el correo electrónico, y el reductor se encarga de alterar el estado con esa información.
Pasemos a algo más interesante.
¿Cómo haces que ese filtro interactúe con tu grilla de freelancers creada previamente?
Sencillo. Solo tienes que escuchar la parte de filtro de la tienda. Veamos cómo se ve el código.
import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import { IFilter, ACTIONS as FilterACTIONS } from './../filter/filter-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; public filter: Rx.Observable<IFilter>; constructor(private store: Store<AppState>) { this.freelancers = Rx.Observable.combineLatest(store.select('freelancers'), store.select('filter'), this.applyFilter); } applyFilter(freelancers: Array<IFreelancer>, filter: IFilter): Array<IFreelancer> { return freelancers .filter(x => !filter.name || x.name.toLowerCase().indexOf(filter.name.toLowerCase()) !== -1) .filter(x => !filter.email || x.email.toLowerCase().indexOf(filter.email.toLowerCase()) !== -1) } ngOnInit() { } delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) } }No es más complicado que eso.
Una vez más, usó el poder de RxJS para combinar el filtro y el estado de los trabajadores independientes.
De hecho, combineLatest se activará si uno de los dos observables se activa y luego combinará cada estado mediante la función applyFilter . Devuelve un nuevo observable que lo hace. No tenemos que cambiar ninguna otra línea de código.
Observe cómo al componente no le importa cómo se obtiene, modifica o almacena el filtro; solo lo escucha como lo haría para cualquier otro estado. Acabamos de agregar la funcionalidad de filtro y no agregamos nuevas dependencias.
haciéndolo brillar
¿Recuerdas que el uso de Ngrx realmente brilla cuando tenemos que lidiar con datos en tiempo real? Agreguemos esa parte a nuestra aplicación y veamos cómo funciona.
Presentamos el freelancers-service .
ng generate service freelancerEl servicio autónomo simulará la operación en tiempo real de los datos y debería tener este aspecto.
import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-grid/freelancer-reducer'; import { Http, Response } from '@angular/http'; @Injectable() export class RealtimeFreelancersService { private USER_API_URL = 'https://randomuser.me/api/?results=' constructor(private store: Store<AppState>, private http: Http) { } private toFreelancer(value: any) { return { name: value.name.first + ' ' + value.name.last, email: value.email, thumbail: value.picture.large, } } private random(y) { return Math.floor(Math.random() * y); } public run() { this.http.get(`${this.USER_API_URL}51`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.FREELANCERS_LOADED, payload: response.json().results.map(this.toFreelancer) }) }) setInterval(() => { this.store.select('freelancers').first().subscribe((freelancers: Array<IFreelancer>) => { let getDeletedIndex = () => { return this.random(freelancers.length - 1) } this.http.get(`${this.USER_API_URL}${this.random(10)}`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.INCOMMING_DATA, payload: { ADD: response.json().results.map(this.toFreelancer), DELETE: new Array(this.random(6)).fill(0).map(() => getDeletedIndex()), } }); this.addFadeClassToNewElements(); }); }); }, 10000); } private addFadeClassToNewElements() { let elements = window.document.getElementsByClassName('freelancer'); for (let i = 0; i < elements.length; i++) { if (elements.item(i).className.indexOf('fade') === -1) { elements.item(i).classList.add('fade'); } } } }Este servicio no es perfecto, pero hace lo que hace y, con fines de demostración, nos permite demostrar algunas cosas.
Primero, este servicio es bastante simple. Consulta una API de usuario y envía los resultados a la tienda. Es una obviedad, y no tiene que pensar a dónde van los datos. Va a la tienda, que es algo que hace que Redux sea tan útil y peligroso al mismo tiempo, pero volveremos sobre esto más adelante. Después de cada diez segundos, el servicio selecciona algunos autónomos y envía una operación para eliminarlos junto con una operación para algunos otros autónomos.
Si queremos que nuestro reductor pueda manejarlo, debemos modificarlo:
import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, } export const ACTIONS = { LOAD_FREELANCERS: 'LOAD_FREELANCERS', INCOMMING_DATA: 'INCOMMING_DATA', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.INCOMMING_DATA: action.payload.DELETE.forEach((index) => { state.splice(state.indexOf(action.payload), 1); }) return Array.prototype.concat(action.payload.ADD, state); case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }Ahora somos capaces de manejar este tipo de operaciones.
Una cosa que se demuestra en ese servicio es que, de todo el proceso de cambios de estado que se hace de manera sincrónica, es bastante importante notar eso. Si la aplicación del estado fue asíncrona, la llamada a this.addFadeClassToNewElements(); no funcionaría ya que el elemento DOM no se crearía cuando se llame a esta función.
Personalmente, lo encuentro bastante útil, ya que mejora la previsibilidad.
Creación de aplicaciones de forma reactiva
A través de este tutorial, ha creado una aplicación reactiva utilizando Ngrx, RxJS y Angular 2.
Como has visto, estas son herramientas poderosas. Lo que ha construido aquí también puede verse como la implementación de una arquitectura Redux, y Redux es poderosa en sí misma. Sin embargo, también tiene algunas limitaciones. Mientras usamos Ngrx, esas restricciones inevitablemente se reflejan en la parte de nuestra aplicación que usamos.
El diagrama de arriba es un borrador de la arquitectura que acabas de hacer.
Puede notar que incluso si algunos componentes se influyen entre sí, son independientes entre sí. Esta es una peculiaridad de esta arquitectura: los componentes comparten una dependencia común, que es la tienda.
Otra cosa particular de esta arquitectura es que no llamamos funciones sino que despachamos acciones. Una alternativa a Ngrx podría ser crear solo un servicio que administre un estado particular con observables de sus aplicaciones y llamar a funciones en ese servicio en lugar de acciones. De esta manera, podría obtener la centralización y la reactividad del estado mientras aísla el estado problemático. Este enfoque puede ayudarlo a reducir la sobrecarga de crear un reductor y describir acciones como objetos simples.
Cuando siente que el estado de su aplicación se actualiza desde diferentes fuentes y comienza a convertirse en un desastre, Ngrx es lo que necesita.
