Surtout pas de Redux avec Angular !

Pourquoi utiliser Redux avec le framework JavaScript Angular est une très mauvaise pratique. Illustration avec une mini application, simple mais universelle.

Il paraît que les ingénieurs de Google qui ont conçu Angular ne savent pas programmer un two-way data binding correctement, mais heureusement il y a les petits gars de Redux qui sont là pour les sortir de la mouise. Tel est le conte de fée que j’entends maintenant trop souvent. Je veux donc ici remettre les choses à leur place en expliquant pourquoi utiliser Redux avec Angular est une très mauvaise pratique, et je propose une mini appli, simple mais universelle, qui démontre mon propos.

Pour le faire bref Redux est un store dans lequel on enregistre l’état des variables de l’application. Les variables sont associées à des actions qu’il faut opérer en cas de changement d’état, et on peut y souscrire pour être notifié à chaque modification. Cela permet de modifier une variable dans un composant de l’application et d’en propager le nouvel état aux autres composants qui s’y sont abonnés. Ce fonctionnement est la description même du two-way data binding. Et de fait Redux est utilisé avec des bibliothèques JavaScript qui ne sont pas two-way data binding, comme React par exemple, pour justement acquérir cette fonctionnalité. Bien sûr cela implique que vous développiez vous-même votre two-way data binding, propre à chaque application, c’est-à-dire que vous programmiez du Redux, pour chaque variable, en plus du React (ou tout autre bibliothèque qui n’est pas two-way data binding).

Du côté d’Angular, il n’y a pas ce problème, car il est nativement two-way data binding. Un store enregistrant, scrutant et propageant l’état de ses variables est simplement un service natif Angular. L’avantage c’est qu’il n’y a aucune bibliothèque supplémentaire à importer, ni aucune ligne de code à écrire, pour obtenir cette fonctionnalité puisqu’elle est structurelle d’Angular. Il suffit d’instancier une variable dans le store pour qu’elle soit disponible en two-way data binding pour tous les composants qui importent le store. Si une modification est apportée à une variable du store dans un composant, alors tous les autres composants utilisant la même variable sont automatiquement remis à jour, sans avoir à écrire la moindre ligne de code. J’en donne une illustration dans la seconde partie de cet article.

Vous comprenez dès lors qu’il n’y a aucun intérêt à embarquer Redux dans Angular, car ce serait reprogrammer laborieusement un two-way data binding au lieu de se servir de celui qui a été prévu nativement par les ingénieurs de Google. C’est une perte de temps et une augmentation de la complexité, donc au final aussi une augmentation des délais et des coûts, pour un bénéfice fonctionnel totalement nul.

Ah oui, mais avec Redux on peut implémenter la fonctionnalité de back/forward du parcours utilisateur tout au long de ses clics, me rétorque-t-on. En effet tous les états des variables sont enregistrés dans un historique en Redux (concept d’immutabilité), ce qui permet de faire des retours en arrière sur l’état de l’application, puis éventuellement des retour en avant.

Rien de plus simple en Angular : il suffit d’enregistrer le store dans un historique à chaque clic intéressant de l’utilisateur, en pratique dans un tableau (array). A tout instant si vous remplacez le store courant par l’un des stores contenus dans ce tableau historique, le two-way data binding appliquera instantanément le nouveau store dans tous les composants concernés, et cela toujours sans avoir à écrire une seule ligne de code. Si vous voulez que votre historique soit partagé, il faudra le concevoir lui aussi comme un service Angular, qu’il s’appuie pour son stockage sur une array dans le code, une indexedDB ou même le serveur.

Donc Redux n’est à utiliser dans Angular ni pour reprogrammer le two-way data binding, ni pour obtenir la fonctionnalité back/forward. Il est inutile à Angular, qu’il rendrait inutilement complexe et dépendant de ses évolutions.

A cela s’ajoute au passage le problème des observables, que Redux ne gère pas. Ainsi, il faut stocker des observables dans Redux, et les résoudre dans les templates HTML des composants Angular grâce à d’affreux "|async". Ici encore, pourquoi se compliquer la vie puisque Angular gère quant à lui l’asynchronisme à la perfection. Il suffit d’utiliser l’elvis operator (?.) dans les templates HTML pour gérer les variables asynchrones.

À tous ces arguments de bon sens on ne me répond jamais qu’ils sont faux.

En revanche on m’oppose une légende urbaine : le two-way data binding d’Angular ne tiendrait pas la route, surtout pour des applications complexes. Ainsi, les piteux ingénieurs de Google travailleraient mal, et heureusement qu’il y a les petits gars de Redux pour venir les sortir de la mouise. Il y a de quoi rire, ainsi les concepteurs de la machine virtuelle JavaScript la plus performante, les concepteurs de Node.js et d’Angular auraient des faiblesses en JavaScript … Ça laisse pantois.

En réalité les ingénieurs de Google savent très bien où ils vont avec Node en back et Angular en front : ils veulent écraser la concurrence. Il faut une bonne culture JavaScript et avoir mangé beaucoup de JQuery pour comprendre l’importance du two-way data binding, et donc la révolution que fut Angular pour les frontenders. D’un coup on peut développer rapidement des applications complexes, autrefois inimaginables dans un temps raisonnable. Il est là l’objectif des ingénieurs de Google, faciliter le travail du frontender pour réduire les délais et les coûts du développement d’une application web front. Et React+Redux ne font effectivement pas le poids face à Angular, car ils imposent un codage inutile en Angular. A titre d’exemple je viens de développer seul, en trois mois seulement la v12 d’oceanvirtuel.eu, back Node, front Angular 7 et tests compris. Je mets quiconque au défit de développer une application aussi complexe en si peu de temps avec React+Redux (ou pire Angular+Redux). Au final développer avec React+Redux coûte beaucoup plus cher que de le faire en Angular natif, car il y a forcément plus de code à écrire. C’est donc par l’argument du porte monnaie que les ingénieurs de Google veulent écraser la concurrence, et c’est pour cela qu’ils ont pris grand soin à développer leur two-way data binding, car c’est la clé du développement front, et donc là qu’on peut agir pour réduire drastiquement les coûts de développement. Les sous-estimer en prétendant qu’ils ont besoin de Redux pour rendre une copie propre, c’est partir dans la mauvaise direction. En général cela relève d’une méconnaissance du fonctionnement des frontends JavaScript, et de fait ce sont en majorité des backenders nouvellement arrivés sur le front qui s’acharnent à vouloir utiliser du Redux dans Angular.

Le frontend est un métier à part entière, comme l’est le backend, et croire qu’un développeur Java saura forcément programmer de l’Angular est une lourde erreur car les technologies, les concepts et les méthodes de ces deux univers n’ont rien à voir. Qui croit tout savoir faire, fait tout moyennement. Pour ma part je n’ai cessé d’y être confronté lors de mes missions d’expert Angular pendant ces deux dernières années. Lorsque les backenders se mettent à Angular, j’ai systématiquement constaté l’impéritie de leur développement. Et j’ai aussi malheureusement constaté leur entêtement à croire que l’expert se trompe car ce qu’il dit les dérange, même si ce dernier leur prouve par le code les bonnes pratiques. Une de mes dernières missions par exemple, où j’ai recodé en 2 jours le travail de 3 backenders pendant 1 mois. Ils avaient introduit Redux dans Angular. Et que croyez-vous qu’il arrivât ? Hé bien un gros "c’est vrai, tu prouves que tu as raison code à l’appui… mais on va persister car on pense malgré tout que notre méthode est la meilleure". Ces gars là ont bien sûr explosé leurs coûts et leurs délais, pour un résultat très moyen. Le client final a donc apprécié cela tout aussi moyennement, à tel point qu’il a décidé de ne plus utiliser Angular, se coupant ainsi du framework le plus performant du marché. Total gâchis.

J’insiste enfin sur un point important : le store est une excellente pratique pour toute application web, il est même indispensable. C’est lui qui va structurer votre application dont il en est en fait le data model. Il faut donc prendre un très grand soin à définir la structure de votre store, car il servira de référence à tous les développeurs. Il faut imposer des règles fortes avant d’autoriser à ajouter une variable dans le store, sous peine de se retrouver avec une jungle inextricable. En ce sens typer le store est une pratique à conseiller. Et ceci est vrai en Angular comme en Redux. Notez aussi au passage que l’utilisation d’un store réduit au maximum l’utilisation des @Input et @Output d’Angular qui deviennent vite ingérables lorsque l’application se complexifie. C’est une pratique à éviter, autant que faire se peut, en la réservant uniquement aux composants qui ont vocation à être réutilisés hors contexte, dans une autre application par exemple.

La théorie étant dite, passons maintenant aux travaux pratiques. Ci-dessous se trouve une mini application qui démontre comment on utilise un service store two-way data binding et l’asynchronisme dans Angular. Vous pourrez simplement copier/coller ce code chez vous en local pour vérifier qu’il fonctionne.

La technique Angular du store as a service

Cette mini appli va chercher les données d’un customer de façon asynchrone, les range dans un service store Angular nativement two-way data binding, et utilise ce dernier dans deux composants types. Le premier ne fait qu’afficher une variable du store, tandis que l’autre l’affiche et la modifie par un input HTML, provoquant la mise à jour automatique du premier composant.

Notez que pour simplifier je ne vous montre pas ici la partie module qui déclare les services dans les providers, ni le template qui affiche les deux composants, car c’est de l’Angular élémentaire que vous connaissez bien si vous comprenez le code qui suit.

1) Le store

import { Injectable } from '@angular/core';
@Injectable({providedIn: 'root'}) export class StoreService {
    customer: any;
    constructor() { }
}

Notez que le store est un service standard d’Angular, dans lequel on ne fait que déclarer la variable customer. Je n’ai pas typé le contenu du store, mais bien sûr cela serait une meilleure pratique.

2) Le service API

import { Injectable } from '@angular/core';
import { StoreService } from './store.service'
@Injectable({ providedIn: 'root' }) export class ApiService {
    constructor( private store: StoreService ) { }
    getData() {         setTimeout(()=> {             this.store.customer = {                 name: 'Bob',                 age: 25             }         }, 2000);     } }

Dans ce service API, on déclare une fonction getData() qui va chercher les données de façon asynchrone. J’utilise ici un timeout de 2 secondes mais la façon de procéder est exactement la même avec un retour d’observable depuis http.get par exemple. Le code compris dans le setTimeout sera alors simplement le code dans la fonction next()de l’observable.

Notez que ce service API importe le store créé précédemment et qu’il instancie immédiatement le customer dans ce store, dès que les données sont disponibles, ici après 2 secondes. Cela évite de transporter des observables et des «|async» jusque dans les templates HTML, car l’elvis operator «?.» suffit à y gérer l’asynchronisme.

3) Un composant utilisant le store

import { Component, OnInit } from '@angular/core';
import { ApiService } from '../api.service'
import { StoreService } from '../store.service'
@Component({     selector: 'app-mycomponent',     templateUrl: './mycomponent.component.html',     styleUrls: ['./mycomponent.component.css'] }) export class MycomponentComponent implements OnInit {
    constructor(         private api: ApiService,         private store: StoreService     ) { }
    ngOnInit() {         this.api.getData();     } }

Notez qu’à part les imports et les déclaration des services api et store, le composant ne possède qu’une seule ligne de code, celle qui appelle les données. Cette ligne serait même inutile, ainsi que l’import du service api, si un autre composant, ou un autre service de setup par exemple, avait déjà appelé précédemment api.getData(), pour nourrir le store.

4) Le template HTML du composant

<ul>
    <li>Name : {{store.customer?.name}}</li>
    <li>Age : {{store.customer?.age}}</li>
</ul>

Notez qu’on utilise directement le store dans le template, et cela afin de préserver le two-way data binding avec les autres composants qui utilisent le store. La déclaration d’une variable locale au composant ne se justifie que si cette variable n’a pas vocation à se propager ailleurs dans l’application.

Notez aussi l’utilisation de l’elvis operator «?.» qui permet de gérer l’asynchronisme de l’instanciation de customer. Aucun besoin de «|async».

Toute modification du store.customer, depuis le DOM (input par exemple) ou depuis le script du composant, sera alors immédiatement répercutée sur tous les autres composants et services qui importent le même store. Montrons cela en ajoutant un second composant.

5) Un autre composant, qui modifie le store

import { Component, OnInit } from '@angular/core';
import { StoreService } from '../store.service'
@Component({     selector: 'app-myothercomponent',     templateUrl: './myothercomponent.component.html',     styleUrls: ['./myothercomponent.component.css'] }) export class MyothercomponentComponent implements OnInit {
    constructor(private store: StoreService) { }
    ngOnInit()
}

Le composant précédent ayant déjà rempli le store avec les données, celui-ci ne fait qu’importer le store, sans aucune autre ligne de code.

6) Le template HTML du composant qui modifie le store

<p>
    <input type="text" [(ngModel)]="store.customer && store.customer.name">
</p>

Notez qu’on utilise encore ici directement le store dans le template. Notez aussi la façon particulière de gérer l’asynchronisme, qui est du à l’utilisation de [(ngModel)] qui impose cette contrainte. Pour gérer l’input, pensez à importer le FormsModule dans votre app.module (ou autre module support).

En modifiant la valeur de l’input par ce template, vous verrez la modification se propager instantanément au premier composant qui ne fait qu’afficher store.data.customer.

J’en termine avec cet article en vous proposant maintenant de reproduire cette mini appli en utilisant Redux dans votre Angular. Vous pourrez alors comparer objectivement le temps et la complexité du développement pour les deux solutions. Et c’est bien le diable si vous ne parvenez pas à la même conclusion que moi : vive les stores, mais de grâce, natifs Angular et pas Redux !