Resources :
- https://www.opencodez.com/web-development/reactjs-tutorial-5-react-flux-and-redux.htm
- https://www.youtube.com/watch?v=poQXNp9ItL4 (Mosh)
Flux
Flux est une architecture d'application qui a 3 principes principaux :
- Single Source of Truth (source unique de vérité) :
L'état global de votre application est stocké dans une arborescence d'objets au sein d'un même "store" .
Cela facilite la création d'applications universelles, car l'état de votre serveur peut être sérialisé et importé dans le client sans effort de codage supplémentaire. Une arborescence d'état unique facilite également le débogage ou l'inspection d'une application; il vous permet également de conserver l'état de votre application en cours de développement, pour un cycle de développement plus rapide. Certaines fonctionnalités qui étaient traditionnellement difficiles à implémenter - Annuler / Rétablir, par exemple - peuvent soudainement devenir triviales à implémenter si tout votre état est stocké dans une seule arborescence. -
Le State est en lecture seule :
La seule façon de changer l'état est d'émettre une "action", un objet décrivant ce qui s'est passé.
Cela garantit que ni les vues ni les rappels réseau ne modifieront jamais directement le state. Au lieu de cela, ils expriment une intention de transformer l'État. Parce que tous les changements sont centralisés et se produisent un par un dans un ordre strict et que les actions ne sont que des objets simples, elles peuvent être journalisées, sérialisées, stockées et rejouées ultérieurement à des fins de débogage ou de test. -
Les modifications sont effectuées avec des fonctions pures
Pour spécifier comment le state est transformé par les actions, vous écrivez des fonctions "reducer" purs. Les reducer ne sont que des fonctions pures qui prennent l'état précédent et une action, et retournent l'état suivant. N'oubliez pas de renvoyer de nouveaux objets d'état, au lieu de modifier l'état précédent. Vous pouvez commencer avec un seul réducteur et, au fur et à mesure que votre application se développe, le diviser en plus petits réducteurs qui gèrent des parties spécifiques de l'arborescence d'état. Étant donné que les réducteurs ne sont que des fonctions, vous pouvez contrôler l'ordre dans lequel ils sont appelés, transmettre des données supplémentaires ou même créer des réducteurs réutilisables pour des tâches courantes telles que la pagination.
Par ailleurs, Flux vous oblige à :
- faire entrer le flux d'information par la même entrée
- garder le même sens de circulation de l'information - (unidirectional data flow)
Avec Flux, nous avons trois parties majeures : le dispatcher, le store et la vue.
Flux utilise les principes du " functionnal programming".
Functional programming
Définition : C'est un paradigme de programmation qui a pour principe de structurer le code de façon à découper les problèmes en de multiples petites fonctions réutilisables.
Function composition
La composition de fonction est une technique pour construire des fonctions complexes à partir de petites fonctions simples et ré-utilisables (cf compose et pipe)
Ex : const test = " Test "; const str = s => s.trim(); const wrapInDiv = s => `<div>${s}</div>`; console.log(wrapInDiv(str(test)));
currying a function : c'est transformer une fonction qui a plusieurs paramètres en une fonction qui n'en a qu'un et qui renvoie une fonction avec le reste des arguments. Les arrow function sont utilisées car elles permettent une meilleure lecture grâce à leur syntaxe allégée.
Exemple :
function add(a) { return function(b) { return a + b; }; } const add2 = a => b => a + b; // (a, b) => a + b console.log(add(1)(5)); console.log(add2(3)(4));
Lodash
Lodash est une bibliothèque js qui facilite notamment la programmation fonctionnelle (pipe, compose...).
Installation : npm i lodash
Exemple utilisant lodash
import { compose, pipe } from "lodash/fp"; let input = " JavaScript "; let output = "<div>" + input.trim() + "</div>"; const trim = str => str.trim(); const wrap = type => str => `<${type}>${str}</${type}>`;// currying const toLowerCase = str => str.toLowerCase(); const transform = pipe(trim, toLowerCase, wrap("span")); // composing console.log(transform(input));
Pure function
Une fonction pure produit toujours les mêmes résultats avec les mêmes arguments et n'a pas d'effets de bord (side effects).
Immuabilité :
Le principe de l'immuabilité est de ne jamais modifier un objet. On en crée de nouveaux à la place. Cela rend notre programme plus prévisible et améliore la capacité de détecter des changements car une comparaison superficielle suffit pour savoir si l'objet a été modifié (l'objet a une référence différente), sinon il faudrait parcourir toutes les propriétés pour les comparer les unes aux autres.
Exemple pour copier un objet dans le cadre de l'immuabilité en vanilla js:
const person = { name: "Bob" };
const new_person = Object.assign({}, person, {name: "Robert", age: "25"});
ou
const new_person = {... person, name: "Robert"};
Attention, ces mécanismes font des copies superficielles (shallow) et non pas des copies profonde (deep).
Shallow copie :
Lors d'une copie superficielle, un nouvel objet est créé qui a une copie exacte des valeurs de l'objet d'origine. Si l'un des champs de l'objet est une référence à d'autres objets, seules les adresses de référence sont copiées, c'est-à-dire que seule l'adresse mémoire est copiée.
Copie complète :
Une copie complète copie tous les champs et crée des copies de la mémoire allouée dynamiquement pointées par les champs. Une copie complète se produit lorsqu'un objet est copié avec les objets auxquels il se réfère.
Comme on le voit, le js n'est pas un langage qui facilite l'immuabilité. On va donc s'appuyer sur des outils tels que "immutagle" ou "immer".
Immer
Immer est un programme léger qui vous permet de travailler dans le principe de l'immuabilité de façon pratique.
Installation : npm -i immer
Exemple de code utilisant la fonction "produce" :
import { produce } from "immer"; let p = { name: "Dylan" }; function publish(person) { return produce(person, draftPerson => { draftPerson.isPublished = true; }); } let updated = publish(p); console.log(p); console.log(updated);
Redux
Redux est une librairie.
L'intérêt de Redux est d'avoir une seule source de vérité (Single Source Of Truth) pour le "state" (l'état) de votre application. L'état est stocké sous la forme d'un simple objet javascript (objet littéral ou plain object) en un seul endroit : le "store".
L'objet "state" est en lecture seule. Si vous voulez le modifier, vous devez émettre une action, qui est un simple objet JavaScript.
Votre application peut s'abonner (subscribe) pour être avertie lorsque le magasin a changé. Lorsque Redux est utilisé avec React, ce sont les composants React qui sont avertis lorsque l'état change et peuvent être rendus à nouveau en fonction du nouveau contenu du store.
Le magasin a besoin d'un moyen de savoir comment mettre à jour l'état du magasin lorsqu'il obtient une action. Il utilise pour cela une fonction JavaScript simple que Redux appelle un "reducer". La fonction de réduction est transmise lors de la création du magasin.
Store
Redux permet de stocker dans un seul objet appelé "the store" tous les états (state) de l'application. Cet objet est "la seule source de vérité" (single source of truth) et il est accessible par tous les composants.
L’objet store possède trois méthodes :
- subscribe qui permet a tout écouteur (listener) d’être notifié en cas de modification du store. Les gestionnaires de vues (comme React) vont souscrire au store pour être notifié des modification et effectuer mettre à jour l’interface graphique en conséquence.
- dispach qui prend en paramètre une action et exécute le reducer qui va, à son tour, mettre à jour le store avec un nouvel état.
- getState qui retourne l’état courant du store. L’objet retourné ne doit pas être modifié.
Reducer
Pour mettre à jour le store, il n'est pas possible de le faire directement puisque redux s'inscrit dans le paradigme de la programmation fonctionnelle et donc de l'immuabilité.
On va utiliser la fonction appelée "reducer" qui reçoit en arguments le "state" et une "action" et qui retourne le nouveau "state" en fonction de l'"action". C'est au programmeur de décider quelle technique il emploie pour assurer l'immuabilité : spread operator, assign, immer (produce) ....
Il y aura en général plusieurs "reducer" dans une application (un par composant).
Action
Action est un objet littéral (plain object) qui représente ce qu'il vient de se passer. On peut l'assimiler à un événement. Cet objet (événement) est envoyé en argument à la méthode "dispatch" de l'objet "store". La méthode "dispach" est l'unique point d'entrée du "store" ce qui va permettre de contrôler plus facilement les actions des utilisateurs. C'est grâce à cela que l'on va pouvoir "loger" toutes les modifications de l'interface (cf Redux dev tools) ou que l'on va pouvoir facilement mettre en place des mécanismes de "défaire et refaire".
Cycle
L'objet "store" va ensuite appeler le "reducer" concerné. Ce dernier va renvoyer le "state" modifié dans un nouvel objet et c'est à nouveau le "store" qui va demander aux composants de l'interface de se mettre à jour.
Exemple simplifié du cycle de mise à jour de l'interface avec redux :
Moyen mnémotechnique : action dit ct'horrible réalisateur.
Principales étapes pour créer une application Redux (sans React)
- Installer redux : npm i redux
- Designer le "store"
- Créer le "store"
- Définir les "actions"
- Créer un ou plusieurs "reducer"
Desing du store
Il faut d'abord designer l'objet store. Ex pour un gestionnaire de bug :
const store = [
{
id:"1",
description: "Description ici",
resolve: false
}
];
Création du store
Exemple de création d'un store dans le fichier store.js :
import {createStore} from 'redux'; import reducer from "./reducer"; const store = createStore(reducer); export default store;
Définition des actions
Les actions sont de simples objets littéraux. Seul la propriété type est obligatoire mais si l'on suit l'architecture de "Flux", il faut également ajouter la propriété "payload". Cette dernière est porteuse de l'information minimum liée à l'action afin de mettre à jour l'interface. Exemple pour une action d'ajout de bug :
{ type: "bugAdded",// BUG_ADD est une syntaxe plus conventionnelle payload: { description: "Description ici"// la description est l'information minimum à faire passer pour que le bug existe } }
Exemple pour une action de suppresion de bug :
{ type: "bugDeleted", payload: { id: 157// l'id est l'information minimum à faire passer pour supprimer le bug } }
Les actions sont "passées" au store via la méthode "dispatch". Cela va se faire dans notre exemple dans le fichier index.js :
import store from './store'; store.dispatch({ type: "bugAdded", payload: { description: "Bug 1" } }); store.dispatch({ type: "bugRemoved", payload: { id: 1 } }); console.log("store : ", store.getState());
Définition du Reducer dans le fichier reducer.js
let lastId = 0; export default function reducer(state = [], action) { // l'état initial est un tableau vide if(action.type === "bugAdded") { return [ ...state, { id: ++lastId, description: action.payload.description, resolve: false } ] } else if (action.type === "bugRemoved") { return state.filter(bug => bug.id !== action.payload.id); } return state; // permet de revenir à l'état initial l'action n'est pas pris en compte }
Exercice
Récupérez cette archive (merci Mosh) puis :
- Désarchivez la et inspectez les 4 fichiers qui la composent,
- Installez l'application (gérée par webpack) :
npm i
- Installer redux :
npm i redux@4.0
- Lancez l'application :
npm start
- Lancez votre client web préféré : http://localhost:9000/
- Votre mission, si vous l'acceptez, est d'adapter l'exemple du gestionnaire de bug à un gestionnaire de carte (avec question et réponse) et qui permettra de gérer l'action "créer une carte" et l'action supprimer une carte
Voir la correction