Gérer les effets de bord avec Redux-Saga
Introduction
Dans le chapitre précédent, nous avons vu comment gérer l’état et le cycle de vie d’une application à l’aide de Redux. Nous avons également vu comment gérer des actions asynchrones (requêtes HTTP par exemple) à l’aide de Redux-Thunk. Il existe plusieurs moyens de gérer les effets de bord d’une application avec Redux, et Redux-Thunk est probablement le plus simple à mettre en œuvre. Dans ce chapitre, nous allons en voir un autre permettant de mettre en place des patterns plus évolués : Redux-Saga (https://github.com/redux-saga/redux-saga).
La première chose permettant de dire que Redux-Saga diffère de Redux-Thunk est qu’il ne vient pas s’intégrer dans le cycle de vie de base de Redux (tel que décrit dans le chapitre précédent : actions, reducer, etc.). Le principe sera de définir des sagas qui seront déclenchées sur des actions données et qui auront la possibilité d’effectuer des traitements, comme lire le state, appeler des fonctions asynchrones, ou déclencher de nouvelles actions. Tout cela s’intégrera dans notre store Redux à l’aide d’un middleware.
L’une des principales difficultés à l’utilisation de Redux-Saga est qu’il repose sur une fonctionnalité de JavaScript assez...
Les générateurs
Dit simplement, un générateur est une fonction renvoyant un itérateur. Commençons donc par présenter ce qu’est un itérateur. Il s’agit d’un objet possédant une interface permettant de :
-
connaître sa valeur courante ;
-
se déplacer à la valeur suivante ;
-
savoir si l’on a atteint la fin de l’itérateur.
Plusieurs implémentations sont possibles ; voici par exemple une fonction permettant, à partir d’un tableau donné, d’obtenir un itérateur pour le parcourir :
const getArrayIterator = array => {
let index = -1
return {
next() {
index++
return {
value: array[index],
done: index === array.length,
}
},
}
}
Ici, les trois actions propres à l’itérateur sont faites au sein d’une seule fonction next, qui renvoie un objet contenant l’attribut value avec la valeur courante et l’attribut done qui est à true si on est au bout.
La manière la plus naturelle d’utiliser cet itérateur est par une boucle while :
let res
do {
res = it.next()
console.log(res)
} while (!res.done)
Notez qu’un itérateur n’est pas nécessairement fini. Il peut par exemple être pratique d’obtenir un itérateur parcourant une suite infinie de nombres, comme la suite de Fibonnacci :
const getFibonacciIterator = () => {
let previous = 0
let current = 1
return {
next() {
const next = current + previous
previous = current
current = next
return { value: previous, done: false }
}
}
}
Parcourir cet itérateur avec une boucle while causerait bien évidemment quelques problèmes, mais on peut parcourir les valeurs une à une, par exemple dans la console de votre navigateur :
> const fibIt = getFibonacciIterator()
{next: ƒ}
> fibIt.next() ...
Les effets de Redux-Saga
Le but principal de Redux-Saga est de proposer une manière de gérer des effets de bord dans une application utilisant Redux. Pour cela, de la même manière que dans notre exemple précédent où nous déclenchions des effets pour demander une saisie de l’utilisateur ou lui afficher une valeur, nous allons déclencher des effets de Redux-Saga, cette fois-ci pour interagir avec Redux ou déclencher d’autres effets de bord.
Les effets qu’il est possible de déclencher dans Redux-Saga sont de plusieurs types. L’effet call par exemple permet d’appeler une fonction, éventuellement asynchrone, et d’en récupérer le résultat. Typiquement, il s’agit d’une fonction ayant justement des effets de bord, comme une requête à une API.
Des effets permettent de manipuler le store de Redux :
-
select lit une valeur dans le store grâce à un sélecteur, de manière semblable au hook useSelector.
-
put dispatche une action.
-
take attend l’arrivée d’une action d’un type donné.
Enfin, des effets permettent de manipuler l’exécution de la saga elle-même : fork pour dupliquer l’exécution de la saga courante, delay pour attendre une certaine durée, etc.
Afin de mieux comprendre ces effets et voir comment les utiliser, démarrons un exemple simple. Celui-ci ne fera que créer un store Redux, pas de React pour le moment.
Initialisons l’application :
$ yarn init
$ yarn add -D parcel-bundler
$ yarn add redux redux-saga core-js regenerator-runtime
Notez que nous installons les dépendances core-js et regenerator-runtime, ce qui va permettre d’utiliser les fonctions génératrices nécessaires à Redux-Saga (tous les navigateurs ne les supportent pas encore).
Créons le fichier public/index.html avec juste le contenu suivant :
<script src="../src/index.js"></script>
N’oublions pas d’ajouter le script start au package.json :
"scripts": {
"start":...
Un exemple plus complet
Nous allons réaliser une petite application permettant d’interroger une API de recherche d’adresse proposée par l’administration française : adresse.data.gouv.fr. Celle-ci permet à partir d’une requête (un texte libre) de récupérer une liste de résultats correspondant à des adresses réelles, avec des informations comme leur géolocalisation exacte.
Les cas d’utilisations possibles sont nombreux : autocomplétion sur un champ de saisie d’adresse, recherche d’adresse sur une carte, etc. L’avantage pour notre exemple est qu’à ce jour aucune clé n’est nécessaire pour l’utiliser l’API, elle est donc très simple à appeler (pas d’inscription nécessaire notamment).
Notre application présentera un champ de saisie pour la requête, un bouton permettant de lancer la recherche, et une liste de résultats. Elle devra gérer un affichage spécifique pour les cas suivants :
-
la recherche n’a pas encore été lancée
-
la recherche est en cours
-
une erreur s’est produite
-
aucun résultat n’est renvoyé
-
au moins un résultat est renvoyé

Notre recherche d’adresse en action
L’intégralité de l’état de notre application sera stockée dans le state de Redux (et notamment le contenu du champ de saisie), et cela va de soi, nous utiliserons Redux-Saga pour exécuter la recherche.
Pour initialiser l’application, je vous suggère de dupliquer l’exemple précédent, et d’y ajouter les dépendances react, react-dom et react-redux.
Commençons par écrire la partie Redux de notre application. Comme dans le chapitre précédent, organisons cela en services pour plus de clarté, même si pour notre exemple nous n’en aurons qu’un seul : le search service.
Notre service proposera une action pour mettre à jour la requête de recherche (ce qui sera affiché dans le champ de saisie), et des actions pour mettre à jour l’état de la recherche. Jusque-là, cela est très similaire à ce que nous faisions avec Redux-Thunk dans le chapitre précédent.
// src/services/search/actions.js ...
Conclusion
Dans le chapitre précédent, nous avions vu comment architecturer une application React autour de Redux, et gérer les effets de bord avec Redux-Thunk. Dans ce chapitre nous avons découvert une méthode plus puissante bien que plus complexe pour gérer ces effets : Redux-Saga. Ce dernier permet des patterns beaucoup plus avancés, et nous n’en avons vu qu’une petite partie ici. Il est possible de faire des « courses » (race) entre plusieurs sagas, d’en annuler certaines, ou encore de surveiller autre chose que le store de Redux.
N’ayez crainte, ce que nous avons vu ensemble devrait déjà correspondre à la plupart des besoins que vous rencontrerez dans votre utilisation de Redux. Et déjà avec ces notions vous êtes capables de mettre en place des traitements évolués.
Si vous vous retrouvez à utiliser Redux-Saga dans le cadre d’une application professionnelle qu’il est nécessaire de tester unitairement, vous pourrez découvrir dans le chapitre Tester une application React de ce livre la bibliothèque Redux Saga Test Plan (http://redux-saga-test-plan.jeremyfairbank.com/), permettant d’écrire des tests très lisibles et pertinents sous forme de « plan de test ». Cela découle directement du modèle adopté par Redux-Saga et de ses effets.
Voilà maintenant...