Le State management pour les webapps avec Redux

Introduction

Problématique

Dans une web app simple, par défaut on va permettre à nos composants de consommer de la donnée : un composant peut lire, créer ou éditer une donnée “A” , tandis qu’un autre peut lui aussi modifier cette donnée “A”, et qu’un troisième composant va pouvoir éditer une propriété de la donnée “A”. Il est facile d’avoir la référence d’un objet distribué à droite à gauche et de permettre un accès vers de multiples composants.

Aïe, le problème avec ce genre de process est qu’il se schématise par une grande toile d’araignée d’accès et de modification de donnée un peu partout, chaque cas totalement dépendant du contexte, ce qui devient très dur à suivre et à debugger …

State management

C’est là qu’intervient la notion de State Management. Pour le moment on peut considérer qu’un State représente plusieurs éléments :

L’idée de base est de pouvoir représenter l’état d’une application en un seul endroit afin d’obtenir un data flow one way. On peut voir le comportement problématique et la solution que nous souhaitons apporter par le schema suivant :

47133006-86ab-4c72-b9d9-a2c18c18cce3_global dataflow.png

Pour se faire on va voir l’architecture proposée par Redux.

Qu’est ce que Redux

redux

Redux est une simple librairie Javascript , agnostique de tout framework SPA ou autre ( bien que couramment utilisé dans un environnent React ). Il met à disposition un pattern de gestion d’état via plusieurs éléments représentant un data flow oneway cyclique.

Les principes

Voici les 3 principes de Redux :

Ces fonctions sont apppelées “Reducers”.

Dataflow

Maintenant que les principes de base sont posés, nous allons voir les éléments qui composent le flux de donnée de notre pattern suivi d’un exemple concret:

Tout ceci représente notre Store, qui est la brique de gestion d’état et qui peut être représentée comme suit :

dataflow

Pour bien comprendre le flux, un composant A représentant un formulaire HTML, souhaite soumettre un champ texte dont la valeur doit s’afficher à l’écran.

La soumission du formulaire va donc appeler un dispatcher en lui précisant une action dont le type est: “ADD_TEXT” et le paylod : “Lorem Impsum”.

Le dispatcher, lui, va contacter le réducer correspondant au type d’action et lui demander de mettre à jour l’état de notre application. Le reducer retournera donc un nouveau State qui contiendra la valeur à notre composant initial.

Exemple simpliste d’implémentation

Postulat

la vue:

1
2
3
4
5
6
<div>
  <ul id="resultat"></ul>
  <input id="firstname" type="text" value="" />
  <input id="lastname" type="text" value="" />
  <button id="validate" type="submit">Valider</button>
</div>

le code behind

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var main = function () {
      var submitButton = document.getElementById("validate");
      var resultsHtml = document.getElementById("resultat");
 
      var currentFirstname = document.getElementById("firstname");
      var currentLastname = document.getElementById("lastname");
 
      var results = [];
       
      function init() {
          submitButton.addEventListener("click", (e) => {
            results.push({
              firstname: currentFirstname.value,
              lastname: currentLastname.value
            });
            refreshResults();
          });
      }
       
      function refreshResults(){
        var resultsAsString = '';
        results.forEach((item) => {
          resultsAsString += `<li>${item.firstname} ${item.lastname}</li>`;
        });
        resultsHtml.innerHTML = resultsAsString;
      }
      return {
        init: init
      };
    }();

Dans cet exemple, nous avons simplement un formulaire, et lors du clic sur le bouton de validation, nous voulons afficher la nouvelle entrée dans une liste, le tableau “results” est accessible par tous, et peut être modifié à n’importequel moment, voyons donc comment faire en sorte qu’il soit représenté dans un State.

Rajout du Store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 function reducer(action) {
       switch (action.type) {
         case "ADD_PERSON":
           var currentPayload = action.newPerson;
           var newPayload = [...state.persons, currentPayload]
           // nouveau State
           return { persons: newPayload }
       }
       return state;
     }

/!\ Remarque : on voit bien dans l’exemple du reducer la notion d’immutabilité, on créé un nouveau payload ainsi qu’on retourne un nouveau State englobant notre payload. A noter l’utilisation du “spread operator” javascript “…” qui sert à ré-assigner les propriétés dans l’objet courant. Voir aussi : Object.assign() ou Function.prototype.apply() mais attention au DeepClone et au Shallow-cloning.

Il ne reste plus qu’à dispatcher une action, voici l’évolution de notre handler pour l’évenement click:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 submitButton.addEventListener("click", (e) => {
        var action = {
          type: "ADD_PERSON",
          newPerson: {
            firstname: currentFirstname.value,
            lastname: currentLastname.value
          }
        };
 
        store.dispatch(action);
      });

Ok, à ce stade nous informons le store qu’une nouvelle valeur s’ajoute à notre tableau “persons” et que l’on doit donc faire évoluer le State. Cependant comment récupérer le nouveau State depuis un composant ? Nous allons ajouter des souscription !

Subscribe & Notify

Rajoutons donc à notre store des subscribers, une methode de subscribe, et une de notify comme suit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//subscribers
var subscribers = [];
 
//getter
function getState() {
  return state;
}
 
//notify
function notify() {
  subscribers.forEach((sub) => {
    sub(getState());
  });
}
 
//subscribe
function subscribe(fn) {
  subscribers = [...subscribers, fn];
  notify();
}

Maintenant nous touchons au but, mais attention, ne pas oublier d’appeler le notify lors d’un dispatch évidement et il nous reste plus qu’à nous abonner dans la methode “init” de notre “main”comme suit :

1
2
3
4
 store.subscribe((newState) => {
    results = newState.persons;
    refreshResults();
  });

Voila !

Maintenant n’importequel composant peut s’abonner et se refresh à la moindre modification de donnée. Code complet ici ==> https://jsfiddle.net/mv9jads1/

Quelle solution

Il existe plusieurs librairies JS, la plupart basées sur Redux, sensiblement similaires plus ou moins adaptées en fonction du projet comme par exemple :

Conclusion

La notion de state management nous apporte les bénéfices suivants

Voila pour la partie théorique je vous propose maintenant de suivre sur l’article suivant dont le sujet est l’implémentation avec ngrx dans le cadre d’une application Angular. Rendez-vous par là -> Librairies NGRX pour une application Angular réactive. Part 13 : ngrx/store