Dans cet article, nous allons nous intéresser à la libraire Redux développé par Dan Abramov, membre de la core team React.

Redux est une des implémentations les plus populaires du pattern Flux, introduit par Facebook et permettant une gestion centralisée d’une application web.

Pour résumer rapidement, un évènement utilisateur (ex : clic sur un bouton) va déclencher une action, qui va mettre à jour l’état global de l’application et en retour, mettre à jour l’interface de l’application.

Mais, pour mieux comprendre le fonctionnement de Redux, nous allons le coder pas à pas pour finalement l’intégrer dans une application React.

Un state global

Le B.A.BA de la librairie Redux, c’est la gestion de l’état global (le state) de notre application. Pour respecter le vocabulaire Redux, nous allons créer un objet store chargé de gérer tout ça.

Cet objet store va donc posséder une variable interne state.
Celui-ci est un simple objet javascript ne pouvant pas être modifier depuis l’extérieur du store.

Par contre, si on ne peut pas le modifier, il faut tout de même pouvoir lire sa valeur. C’est pourquoi on crée une fonction getState qui retourne la valeur du state. Ce dernier n’est en l’état absolument pas générique et réutilisable (nous l’améliorerons par la suite).

Pour l’instant, il contient une clé counter initialisée à 0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = function() {
//notre objet state
let state = { counter: 0 };
//la methode pour accéder au
//au state depuis l'extèrieur
const getState = () => state;
//on retourne les
//fonctions que
//l'on veut rendre accessible
return {
getState,
};
}

Dispatcher une action et modifier le state

En l’état, on ne peut pas modifier la variable counter du state. On va donc, en suivant le vocabulaire de Flux et Redux, permettre de dispatcher un event, c’est à dire informer le store qu’une action pouvant éventuellement modifier le state a eu lieu.

En l’occurence, pour notre exemple, on va incrémenter ou décrémenter counter suivant l’action que l’on envoie. Ici, on va prendre en compte deux actions, ‘ADD’ et ‘SUBSTRACT’ qui vont modifier le state en conséquence.

Cette action va donc être prise en compte par une nouvelle fonction dispatch qui prend en entrée le nom de l’action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Store = function() {
/*
cf. code précédent
*/
const dispatch = action => {
if (action === 'ADD') {
state.counter = state.counter + 1;
} else if (action === 'SUBSTRACT') {
state.counter = state.counter - 1;
}
}
return {
getState,
dispatch,
};
}

Maintenant, en plus de la lecture du state via getState, on peut donc le modifier indirectement avec la méthode dispatch.

Rendre notre store plus générique

Dans l’exemple précédent, le state initial est hardcodé, ce qui n’est pas génial si l’on souhaite faire autre chose qu’un compteur…, ce qui a de forte chance d’arriver :)

On va donc modifier Store de tel sorte qu’il accepte en entrée un argument initialState (qui sera un objet vide par défaut), qui sera utilisé - comme son nom l’indique - pour assigner une état initial à notre state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Store = function(initialState = {}) {
//notre objet state
let state = initialState;
/*
cf. code précédent
*/
};
const initialState = { counter : 0 };
//instanciation de notre store
//avec le state contenant counter
const store = new Store(initalState);

De même, la fonction dispatch définit précédemment contient toute la logique de mise à jour en interne, il faut donc l’externaliser.

Pour réaliser cela, on amène un autre élément essentiel de Redux que l’on avait survolé: le reducer. Ce dernier est une fonction qui prend en argument le state actuel, à l’instant T et l’action qui vient d’être dispatchée pour retourner le state suivant, le nouveau state de notre application.

Le reducer sera le deuxième argument que va recevoir notre objet store à l’instanciation et va venir instancier la variable reducer de notre Store lors de l’instanciation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//store.js
const Store = function(initialState = {}, reducer) {
/*
cf. code précédent
*/
let reducer = reducer;
//l'écriture de dispatch
//est maintenant extrêmement simple
//la logique étant externalisée
const dispatch = action => {
state = reducer(action, state);
}
return {
getState,
dispatch,
};
}

Le reducer est maintenate un fichier à part:

1
2
3
4
5
6
7
8
9
//reducer.js
const reducer = (action, state) => {
if (action === 'ADD') {
return { counter: state.counter + 1 };
} else if (action === 'SUBSTRACT') {
return { counter: state.counter - 1 };
}
return state;
};

Pour instancier le store, on écrit maintenant :

1
2
const store = new Store(initialState, reducer);

Maintenant, on a quelque chose qui commence à ressembler pas mal au code que l’on a quand on utilise Redux. On crée un store en définissant un state initial et le reducer associé qui viendra le modifier.

Maintenant, fini de jouer dans la console javascript: on va tenter de lier ça à React.

React et notre Redux homemade

Nous allons développer une application React très simple mais, pour rendre ça plus sympa à regarder, on va utiliser react-semantic-ui, une librairie qui regroupe plein de composants prêts à l’emploi.

Pour tester notre code avec React, nous allons créer une boîte mail qui affiche de faux mails. Au niveau de l’UI, deux actions seront permises par l’utilisateur via un bouton refresh qui va incrémenter le nombre de faux mails et un bouton supprimer qui va décrémenter le nombre de mails.

Cela nous permet d’utiliser quasiment tel quel le store que l’on vient d’écrire. On l’étoffera par la suite.

React et notre Redux: Etape #1

Première étape, la création d’un composant React qui va accéder au contenu du state et l’afficher.

Dans la méthode render, on va donc appeler la méthode getState du store pour accéder et afficher la valeur de counter.

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
render() {
//on accède au state
const mails = store.getState().counter;
return (
<div style={S.mainWrapper}>
<div style={S.mainTitle}>
<div style={S.title}>
My Inbox
{' '}
</div>
{/*Le composant Label est un composant provenant de semantic-ui */}
<Label>
<Icon name='mail'/>
{/* On affiche ici le nombre de mails */}
{ mails }
</Label>
</div>
<div style={S.refreshButton}>
<Label>
<Icon name='repeat' />
</Label>
</div>
</div>
);
}

Maintenant, on va créer la méthode permettant d’incrémenter la valeur counter du state lorsque l’on clique sur le bouton refresh.

On va donc rajouter une méthode onClick au refresh button via l’ajout de la ligne onClick={this.hitRefreshButton} et écrire ce que va réaliser la méthode hitRefreshButton. En l’occurence, il s’agit ici de dispatcher une action ADD au store :

1
hitRefreshButton = () => store.dispatch('ADD');

Maintenant, essayons de rafraîchir le nombre de mails en cliquant.
Alors…ah, rien ne se passe… Quand, on console.log le state, celui-ci est bien mis à jour, pourtant, le nombre de mails affichés reste désespérement à 0.

Que se passe t-il ?

On a un composant React qui affiche le state contenu dans le store à un instant T et deux boutons qui dispatchent des actions pour mettre à jour le state. Une fois la mise à jour terminée, le composant React est-il au courant qu’il doit effectuer un nouveau rendering ?

NON car personne ne le prévient…

Un composant React va effectuer un nouveau rendering soit :

1) avec la mise à jour des props envoyés par son composant parent
2) en invoquant sa méthode interne setState
3) en invoquant sa méthode interne forceUpdate qui comme son nom l’indique va forcer un re-rendering.

Ici, la première solution n’est pas applicable puisqu’il n’y pas de composant parent.

La seconde ne l’est pas non plus puisque le composant n’a pas de state interne. On pourrait éventuellement créer un state interne pour que setState soit appelé à chaque mise à jour du state global mais, cela amènerait beaucoup de code dupliqué dans chacun des composants utilisant le state dans leur affichage…

La troisième solution semble la plus simple : forcer un re-rendering du composant pour que l’UI affiche la valeur courant du state global. Il faudrait donc qu’à l’issue d’une mise à jour du state, le composant React soit informé qu’il doit effectuer une rendering: un callback.

Pour réaliser cela, nous allons ajouter une méthode subscribe au Store. Cette méthode prendra en argument une fonction que l’on stockera dans un tableau listeners (contenant uniquement des fonctions).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Store = function(initialState = {}, reducer) {
/*
cf code précédent
*/
//le tableau de fonctions
let listeners = [];
//la méthode subscribe qui ajoute une fonction
// au tableau de listeners
const subscribe = fn => listeners.push(fn);
return {
getState,
dispatch,
//on l'ajoute ici au fonction que
//l'objet store expose
subscribe,
};
};

Maintenant, on veut que les fonctions contenues dans listeners soient appelées à chaque mise à jour du state. Ces dernières ont lieu dans la méthode dispatch, c’est donc à cet endroit que l’on va agir.

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
const Store = function(initialState = {}, reducer) {
//notre objet state
let state = initialState;
let listeners = [];
//la methode pour accéder au
//au state depuis l'extèrieur
const getState = () => state;
const subscribe = fn => listeners.push(fn);
const dispatch = action => {
state = reducer(action, state);
//le state vient d'être mis
//à jour, on exécute toutes
//les fonctions contenues
//dans listeners
listeners.forEach(fn => fn());
};
return {
getState,
dispatch,
subscribe,
};
};

Maintenant que le Store est capable de rappeler des callbacks à chaque mises-à-jour, il faut légèrement modifier le composant React pour qu’il fassent partie des listeners du state et se rerender, à chaque mise à jour de ce dernier. On va donc subscriber le composant React pour que sa méthode forceUpdate soit appelée à chaque mise à jour.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//On importe notre store bien sûr !
import store from './redux/store.js';
class App extends Component {
//on veut que notre composant soit
//tout de suite au courant des changements
// de state, donc on réalise le subscribe
//dans componentDidMount
componentDidMount() {
//On fournit un callback à notre store
//tout simplement un fonction qui appelle
//la méthode forceUpdate !
store.subscribe(() => this.forceUpdate());
};
hitRefreshButton = () => {
store.dispatch('ADD');
};
render() {
/* Le rendering */
}

Avec ceci, notre composant se met à jour quand le state change !

React et notre Redux: Etape #2

Un Mail

Pour le fun, j’ai amélioré le prototype afin d’avoir une fausse boîte mail permettant au clic sur le refresh, de générer de nouveaux mails qui peuvent être marqués comme lus ou bien supprimés.

Le store ne contient donc plus uniquement une valeur counter mais des faux mails générés (expéditeur, objet, contenu, etc.) grâce à Faker, une petit librairie très pratique pour mocker du contenu.

De plus, on en profite pour se rapprocher du formalisme de Redux en amenant ici l’écritre de noms d’actions et des fonctions associées :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const ADD = 'ADD';
export const REMOVE = 'REMOVE';
export const MARK_MAIL_AS_READ = 'MARK_MAIL_AS_READ';
export const addMail = () => ({
type: ADD,
});
export const removeMail = index => ({
type: REMOVE,
payload: { index },
});
export const markMailAsRead = index => ({
type: MARK_MAIL_AS_READ,
payload: { index },
});

De cette façon, au sein d’un composant React, on génère un mail de la façon suivante :
store.dispatch(addMail());

On obtient donc ça:

Le code complet est disponible sur Github : https://github.com/GaelS/fake-inbox-redux

Conclusion

En peu de lignes de code, nous avons finalement crée un petit manager de state global que nous avons intégré dans une application React. L’exercice est très sympa pour démystifier Redux, cette librairie incontournable de l’écosystème React.

Maintenant, les limites de l’exercice sont que l’on se retrouve à devoir ajouter une méthode componentDidMount pour forcer l’update de tous les composants dont le rendu est lié au state. De même, on doit logiquement importer le store lorsque l’on souhaite accéder au méthode dispatch, subscribe ou getState.

On verra dans un prochain article comment améliorer cette connexion entre React et Redux…

Teaser: je pense que la librairie react-redux sera notre amie pour l’inspiration :)

Merci de m’avoir lu et n’hésitez pas à laisser un commentaire juste en dessous.