Tutoriel Ngrx et Angular 2 : Construire une application réactive
Publié: 2022-03-11Nous parlons beaucoup de programmation réactive dans le domaine angulaire. La programmation réactive et Angular 2 semblent aller de pair. Cependant, pour quiconque n'est pas familier avec les deux technologies, il peut être assez difficile de comprendre de quoi il s'agit.
Dans cet article, en créant une application Angular 2 réactive à l'aide de Ngrx, vous apprendrez quel est le modèle, où le modèle peut s'avérer utile et comment le modèle peut être utilisé pour créer de meilleures applications Angular 2.
Ngrx est un groupe de bibliothèques angulaires pour les extensions réactives. Ngrx/Store implémente le modèle Redux à l'aide des observables RxJS bien connus d'Angular 2. Il offre plusieurs avantages en simplifiant l'état de votre application en objets simples, en appliquant un flux de données unidirectionnel, etc. La bibliothèque Ngrx/Effects permet à l'application de communiquer avec le monde extérieur en déclenchant des effets secondaires.
Qu'est-ce que la programmation réactive ?
La programmation réactive est un terme que vous entendez beaucoup de nos jours, mais que signifie-t-il vraiment ?
La programmation réactive est une façon dont les applications gèrent les événements et le flux de données dans vos applications. Dans la programmation réactive, vous concevez vos composants et d'autres éléments de votre logiciel afin de réagir à ces changements au lieu de demander des changements. Cela peut être un grand changement.
Un excellent outil pour la programmation réactive, comme vous le savez peut-être, est RxJS.
En fournissant des observables et de nombreux opérateurs pour transformer les données entrantes, cette bibliothèque vous aidera à gérer les événements dans votre application. En fait, avec les observables, vous pouvez voir l'événement comme un flux d'événements et non comme un événement ponctuel. Cela vous permet de les combiner, par exemple, pour créer un nouvel événement que vous écouterez.
La programmation réactive est un changement dans la façon dont vous communiquez entre les différentes parties d'une application. Au lieu de pousser les données directement vers le composant ou le service qui en avait besoin, dans la programmation réactive, c'est le composant ou le service qui réagit aux modifications de données.
Un mot sur Ngrx
Afin de comprendre l'application que vous allez créer à travers ce didacticiel, vous devez vous plonger rapidement dans les concepts de base de Redux.
Boutique
Le magasin peut être considéré comme votre base de données côté client mais, plus important encore, il reflète l'état de votre application. Vous pouvez le voir comme la seule source de vérité.
C'est la seule chose que vous modifiez lorsque vous suivez le modèle Redux et que vous modifiez en lui envoyant des actions.
Réducteur
Les réducteurs sont les fonctions qui savent quoi faire avec une action donnée et l'état précédent de votre application.
Les réducteurs prendront l'état précédent de votre magasin et lui appliqueront une fonction pure. Pure signifie que la fonction renvoie toujours la même valeur pour la même entrée et qu'elle n'a pas d'effets secondaires. A partir du résultat de cette fonction pure, vous aurez un nouvel état qui sera mis dans votre magasin.
Actions
Les actions sont la charge utile qui contient les informations nécessaires pour modifier votre boutique. Fondamentalement, une action a un type et une charge utile que votre fonction de réduction prendra pour modifier l'état.
Répartiteur
Les répartiteurs sont simplement un point d'entrée pour vous permettre de répartir votre action. Dans Ngrx, il existe une méthode d'expédition directement sur le magasin.
Intergiciel
Les intergiciels sont des fonctions qui intercepteront chaque action envoyée afin de créer des effets secondaires, même si vous ne les utiliserez pas dans cet article. Ils sont implémentés dans la bibliothèque Ngrx/Effect, et il y a de fortes chances que vous en ayez besoin lors de la création d'applications réelles.
Pourquoi utiliser Ngrx ?
Complexité
Le magasin et le flux de données unidirectionnel réduisent considérablement le couplage entre les parties de votre application. Ce couplage réduit réduit la complexité de votre application, puisque chaque pièce ne se soucie que d'états spécifiques.
Outillage
L'état complet de votre application est stocké en un seul endroit, il est donc facile d'avoir une vue globale de l'état de votre application et aide pendant le développement. De plus, avec Redux, il y a beaucoup d'outils de développement sympas qui tirent parti du magasin et peuvent aider à reproduire un certain état de l'application ou à faire un voyage dans le temps, par exemple.
Simplicité architecturale
De nombreux avantages de Ngrx sont réalisables avec d'autres solutions ; après tout, Redux est un modèle architectural. Mais lorsque vous devez créer une application parfaitement adaptée au modèle Redux, comme des outils d'édition collaborative, vous pouvez facilement ajouter des fonctionnalités en suivant le modèle.
Bien que vous n'ayez pas à réfléchir à ce que vous faites, ajouter des éléments tels que des analyses à toutes vos applications devient trivial puisque vous pouvez suivre toutes les actions qui sont envoyées.
Petite courbe d'apprentissage
Étant donné que ce modèle est si largement adopté et simple, il est vraiment facile pour les nouvelles personnes de votre équipe de rattraper rapidement ce que vous avez fait.
Ngrx brille le plus lorsque vous avez beaucoup d'acteurs externes qui peuvent modifier votre application, comme un tableau de bord de surveillance. Dans ces cas, il est difficile de gérer toutes les données entrantes qui sont transmises à votre application, et la gestion de l'état devient difficile. C'est pourquoi vous voulez le simplifier avec un état immuable, et c'est une chose que le magasin Ngrx nous fournit.
Construire une application avec Ngrx
La puissance de Ngrx brille le plus lorsque vous avez des données extérieures qui sont transmises à notre application en temps réel. Dans cet esprit, construisons une simple grille de pigistes qui montre les pigistes en ligne et vous permet de les filtrer.
Configuration du projet
Angular CLI est un outil génial qui simplifie grandement le processus de configuration. Vous voudrez peut-être ne pas l'utiliser, mais gardez à l'esprit que le reste de cet article l'utilisera.
npm install -g @angular/cli
Ensuite, vous souhaitez créer une nouvelle application et installer toutes les bibliothèques Ngrx :
ng new toptal-freelancers npm install ngrx --save
Réducteur de pigistes
Les réducteurs sont un élément central de l'architecture Redux, alors pourquoi ne pas commencer par eux en premier lors de la création de l'application ?
Tout d'abord, créez un réducteur "indépendants" qui se chargera de créer notre nouvel état à chaque fois qu'une action est envoyée au magasin.
grille-freelancer/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; } }
Voici donc notre réducteur de pigistes.
Cette fonction sera appelée chaque fois qu'une action est envoyée via le magasin. Si l'action est FREELANCERS_LOADED
, elle créera un nouveau tableau à partir de la charge utile de l'action. Si ce n'est pas le cas, il renverra l'ancienne référence d'état et rien ne sera ajouté.
Il est important de noter ici que, si l'ancienne référence d'état est renvoyée, l'état sera considéré comme inchangé. Cela signifie que si vous appelez un state.push(something)
, l'état ne sera pas considéré comme ayant changé. Gardez cela à l'esprit lorsque vous effectuez vos fonctions de réducteur.
Les états sont immuables. Un nouvel état doit être renvoyé à chaque fois qu'il change.
Composant de grille de pigiste
Créez un composant de grille pour afficher nos pigistes en ligne. Au début, cela ne reflétera que ce qui est dans le magasin.
ng generate component freelancer-grid
Mettez ce qui suit dans 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'); } }
Et ce qui suit dans 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>
Alors qu'est-ce que tu viens de faire ?
Tout d'abord, vous avez créé un nouveau composant appelé freelancer-grid
.
Le composant contient une propriété nommée freelancers
qui fait partie de l'état de l'application contenue dans le magasin Ngrx. En utilisant l'opérateur de sélection, vous choisissez de n'être averti que par la propriété freelancers
de l'état général de l'application. Alors maintenant, chaque fois que la propriété freelancers
de l'état de l'application change, votre observable sera notifié.
Une chose qui est belle avec cette solution est que votre composant n'a qu'une seule dépendance, et c'est le magasin qui rend votre composant beaucoup moins complexe et facilement réutilisable.
Sur la partie template, vous n'avez rien fait de trop complexe. Notez l'utilisation du canal asynchrone dans le *ngFor
. L'observable des freelancers
n'est pas directement itérable, mais grâce à Angular, nous avons les outils pour le déballer et lier le dom à sa valeur en utilisant le tube asynchrone. Cela rend le travail avec l'observable tellement plus facile.
Ajout de la fonctionnalité Supprimer les pigistes
Maintenant que vous avez une base fonctionnelle, ajoutons quelques actions à l'application.
Vous voulez être en mesure de retirer un pigiste de l'État. Selon le fonctionnement de Redux, vous devez d'abord définir cette action dans chaque état qui en est affecté.
Dans ce cas, ce n'est que le réducteur des 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; } }
Il est vraiment important ici de créer un nouveau tableau à partir de l'ancien afin d'avoir un nouvel état immuable.
Maintenant, vous pouvez ajouter une fonction de suppression des pigistes à votre composant qui enverra cette action au magasin :

delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
Cela n'a-t-il pas l'air simple ?
Vous pouvez désormais supprimer un pigiste spécifique de l'État, et ce changement se propagera dans votre application.
Et si vous ajoutiez un autre composant à l'application pour voir comment ils peuvent interagir entre eux via le magasin ?
Filtre Réducteur
Comme toujours, commençons par le réducteur. Pour ce composant, c'est assez simple. Vous voulez que le réducteur renvoie toujours un nouvel état avec uniquement la propriété que nous avons distribuée. Cela devrait ressembler à ceci :
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; } }
Composant de filtre
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, }) } }
Tout d'abord, vous avez créé un modèle simple qui comprend un formulaire avec deux champs (nom et e-mail) qui reflète notre état.
Vous gardez ces champs synchronisés avec l'état un peu différemment de ce que vous avez fait avec l'état des freelancers
. En fait, comme vous l'avez vu, vous avez souscrit à l'état du filtre, et à chaque fois, celui-ci se déclenche en vous affectant la nouvelle valeur au formControl
.
Une chose qui est bien avec Angular 2, c'est qu'il vous fournit de nombreux outils pour interagir avec les observables.
Vous avez vu le tube asynchrone plus tôt, et maintenant vous voyez la classe formControl
qui vous permet d'avoir un observable sur la valeur d'une entrée. Cela permet des choses fantaisistes comme ce que vous avez fait dans le composant de filtre.
Comme vous pouvez le voir, vous utilisez Rx.observable.merge
pour combiner les deux observables donnés par votre formControls
, puis vous faites rebondir ce nouvel observable avant de déclencher la fonction de filter
.
En termes plus simples, vous attendez une seconde après que le nom ou l'e-mail formControl
ait changé, puis appelez la fonction de filter
.
N'est-ce pas génial?
Tout cela se fait en quelques lignes de code. C'est l'une des raisons pour lesquelles vous allez adorer RxJS. Cela vous permet de faire facilement beaucoup de ces choses fantaisistes qui auraient été plus compliquées autrement.
Passons maintenant à cette fonction de filtre. Qu'est ce que ça fait?
Il distribue simplement l'action UPDATE_FILTER
avec la valeur du nom et de l'e-mail, et le réducteur se charge de modifier l'état avec cette information.
Passons à quelque chose de plus intéressant.
Comment faites-vous interagir ce filtre avec votre grille de pigistes créée précédemment ?
Simple. Vous n'avez qu'à écouter la partie filtre du magasin. Voyons à quoi ressemble le code.
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, }) } }
Ce n'est pas plus compliqué que ça.
Une fois de plus, vous avez utilisé la puissance de RxJS pour combiner l'état du filtre et des pigistes.
En fait, combineLatest
se déclenchera si l'un des deux observables se déclenche, puis combinera chaque état à l'aide de la fonction applyFilter
. Il renvoie un nouvel observable qui le fait. Nous n'avons pas à modifier d'autres lignes de code.
Remarquez comment le composant ne se soucie pas de la façon dont le filtre est obtenu, modifié ou stocké ; il l'écoute seulement comme il le ferait pour n'importe quel autre État. Nous venons d'ajouter la fonctionnalité de filtre et nous n'avons ajouté aucune nouvelle dépendance.
Faire briller
Rappelez-vous que l'utilisation de Ngrx brille vraiment lorsque nous devons traiter des données en temps réel ? Ajoutons cette partie à notre application et voyons comment cela se passe.
Présentation du freelancers-service
.
ng generate service freelancer
Le service indépendant simulera le fonctionnement en temps réel sur les données et devrait ressembler à ceci.
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'); } } } }
Ce service n'est pas parfait, mais il fait ce qu'il fait et, à des fins de démonstration, il nous permet de démontrer certaines choses.
Tout d'abord, ce service est assez simple. Il interroge une API utilisateur et envoie les résultats au magasin. C'est une évidence, et vous n'avez pas à vous demander où vont les données. Il va au magasin, ce qui rend Redux si utile et dangereux à la fois, mais nous y reviendrons plus tard. Toutes les dix secondes, le service sélectionne quelques pigistes et envoie une opération pour les supprimer ainsi qu'une opération à quelques autres pigistes.
Si nous voulons que notre réducteur puisse le gérer, nous devons le modifier :
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; } }
Maintenant, nous sommes capables de gérer de telles opérations.
Une chose qui est démontrée dans ce service est que, de tous les processus de changement d'état effectués de manière synchrone, il est très important de le remarquer. Si l'application de l'état était asynchrone, l'appel sur this.addFadeClassToNewElements();
ne fonctionnerait pas car l'élément DOM ne serait pas créé lorsque cette fonction est appelée.
Personnellement, je trouve cela très utile, car cela améliore la prévisibilité.
Créer des applications, de manière réactive
Grâce à ce didacticiel, vous avez créé une application réactive à l'aide de Ngrx, RxJS et Angular 2.
Comme vous l'avez vu, ce sont des outils puissants. Ce que vous avez construit ici peut également être considéré comme la mise en œuvre d'une architecture Redux, et Redux est puissant en soi. Cependant, il a aussi quelques contraintes. Alors que nous utilisons Ngrx, ces contraintes se reflètent inévitablement dans la partie de notre application que nous utilisons.
Le diagramme ci-dessus est une ébauche de l'architecture que vous venez de faire.
Vous remarquerez peut-être que même si certains composants s'influencent mutuellement, ils sont indépendants les uns des autres. C'est une particularité de cette architecture : les composants partagent une dépendance commune, qui est le magasin.
Une autre particularité de cette architecture est que nous n'appelons pas de fonctions mais distribuons des actions. Une alternative à Ngrx pourrait être de créer uniquement un service qui gère un état particulier avec des observables de vos applications et d'appeler des fonctions sur ce service au lieu d'actions. De cette façon, vous pourriez obtenir la centralisation et la réactivité de l'État tout en isolant l'État problématique. Cette approche peut vous aider à réduire les frais généraux liés à la création d'un réducteur et à décrire les actions comme des objets simples.
Lorsque vous sentez que l'état de votre application est mis à jour à partir de différentes sources et que cela commence à devenir un gâchis, Ngrx est ce dont vous avez besoin.