Dans cet article, je vais vous montrer pas à pas comment passer d’une implémentation de code naïve utilisant de nombreux traitements sur un state à une implémentation plus élégante et lisible en utilisant le hook useReducer de React.
Vous trouverez un exemple d’implémentation illustrant les propos de l’article ici.
Introduction
Bip, bip, bip … Vous entendez ? C’est le doux bruit des oiseaux dans les arbres … Non pas du tout, c’est votre réveil ! Et oh ! Debout, il est lundi matin et vous avez du pain sur la planche !
Vous vous réveillez, engloutissez une tasse de café et en selle ! En arrivant au travail, vous ouvrez vos mails et constatez que la nouvelle fonctionnalité à implémenter dans votre application est arrivée !
Votre client veut pouvoir gérer son organisation et vous demande d’implémenter une todo list. Les fonctionnalités sont les suivantes :
- Ajouter une tâche
- Modifier une tâche
- Supprimer une tâche
Dans votre tête vous vous dites : “Pffff, encore une stupide todo list”. Après tout vous êtes un expert React, ça ne devrait pas être compliqué ! Vous vous lancez tête baissée dans le développement.
L’approche naïve
Vous vous dites que partir sur un state est une bonne idée. Pour chaque action, vous appliquez un traitement sur le state courant puis vous mutez le state avec le résultat du traitement.
Vous commencez par déclarer le state comme suit :
const [task, setTasks] = useState(INITIAL_TASKS);
Maintenant que vous avez une liste de tâches qui s’affiche à l’écran, vous pouvez passer aux choses sérieuses. Vous implémentez les trois fonctionnalités demandées par le client. La stratégie est simple, pour chaque fonctionnalité, vous implémenterez un bouton qui appelle une fonction au clique de l’utilisateur.
1. Ajouter une tâche
function addTask(text) {
const newTask = {
id: tasks.length + 1,
text,
}
setTasks([...tasks, newTask]);
}
2. Modifier une tâche
function editTask(taskId, text) {
setTask(tasks.map(task => {
if (task.id === taskId) {
task.text = text,
}
return task;
}));
}
3. Supprimer une tâche
function deleteTask(taskId) {
setTasks(tasks.filter(task => task.id !== taskId));
}
Le travail est fait, vous vous craquez les doigts, admirez 30 secondes votre travail et la satisfaction vous envahit. Le composant fonctionne !
Vous soumettez la Pull Request à votre lead tech et là, c’est le drame ! La PR est refusée. Le commentaire est le suivant : “Le composant est complexe à lire, j’aimerais un composant plus lisible”. Votre lead tech a raison, un programme informatique doit avant tout être lisible par vos pairs.
Vous lisez ce commentaire tout en vous grattant la tête (pas trop sinon vous attraperez une calvitie). Comment pourrais-je avoir un composant plus lisible que le composant actuel ?
Quelle est le problème ?
Si on regarde de plus près votre code, on remarque que pour chaque fonction, vous avez utilisé un setTasks pour muter le state de votre composant. Lorsque vous relisez ce composant, vous devez donc comprendre quelles actions sont possibles, et comment elles sont implémentées.
Ici c’est relativement simple, c’est une todo list mais imaginez des traitements beaucoup plus complexe. Le composant peut vite atteindre une centaine de ligne de logique. Il n’y a rien de pire que de lire un composant possédant autant de ligne qu’un ticket de caisse de courses de Noël.
Utilisez votre cerveau ou votre reducer, c’est la même chose !
Prenons du recul. Qu’est-ce qui doit être implémenté ? L’utilisateur peut appuyer sur trois boutons différents : Ajout, Edition, Suppression. Lorsqu’il clique sur un bouton il demande à l’application de faire une action. Nous avons donc une liste d’évènements utilisateur auquel il faut répondre en faisant un traitement. Nous avons donc un state sur lequel on applique 3 useState.
Prenons un exemple. Votre corps est capable de prouesses diverses et variées. Vous pouvez autant marcher, que courir ou encore chanter. Rien de plus naturel, vous vous dites “je veux me lever”, votre cerveau capte l’action que vous souhaitez faire, met en place l’ensemble des mécanismes nécessaires pour que votre corps passe de l’état “assis” à l’état debout.
Si on reprend notre composant, imaginons que vos actions marcher, courir ou danser soient activables avec des boutons sur votre interface utilisateur, alors lorsqu’on appuie sur le bouton “se mettre debout”, le composant reçoit le signal et le traite passant le state corps de l’état assis à l’état debout.
C’est la multitude d’actions applicable sur notre state qui motive l’utilisation d’un cerveau afin de déléguer la logique de transformation du state au cerveau.
C’est possible grâce au hook useReducer! Alors n’attendons plus une seconde, passons à la partie suivante et branchons un cerveau sur votre composant !
Qu’est ce qu’un reducer ?
Un reducer est ni plus ni moins qu’un hook qui permet d’abstraire tous les traitements d’un state en dehors d’un composant.
Le useReduce se déclare de la façon suivant :
const [state, dispatch] = useReducer(yourReducer, INITIAL_VALUE);
Un reducer est un hook React prenant en paramètre une fonction yourReducer ainsi que l’état initial INITIAL_VALUE.
Le reducer expose le state ainsi qu’une fonction dispatch permettant d’appliquer des actions sur le state.
Le but du reducer est d’exporter la logique appliquée sur le state dans une fonction. On se retrouve donc avec un catalogue d’action applicable sur ce state.
Comme une image vaut mille mots, passons à un exemple. Adaptons votre code qui ne satisfait pas votre lead tech et transformons le avec un reducer.
Passer de useState à useReducer
Nous allons procéder en trois étapes :
- Transformer les setState en dispatch
- Ecrire la fonction de reducer
- Utiliser useReducer dans notre composant
1. Transformer les setState en dispatch
Dans un premier temps, nous allons identifier les fonctions qui utilisent un setState et les transformer en dispatch. Le dispatch permet d’envoyer le stimulus au cerveau “Je veux faire cette action”. Ce stimulus est en fait un paramètre appelé action.
dispatch({
type: 'action_name',
// data
})
Maintenant que vous savez comment dispatch fonctionne, nous allons refacto le code actuel. La stratégie est simple, un setState = 1 dispatch.
// setState
function addTask(text) {
const newTask = {
id: tasks.length++,
text,
}
setTasks([...tasks, newTask ]);
}
function editTask(taskId, text) {
setTasks(tasks.map(task => {
if (task.id === taskId) {
task.text = text;
}
return task;
}));
}
function deleteTask(taskId) {
setTasks(tasks.filter(task => task.id !== taskId));
}
// dispatch
function handleAddTask(text) {
dispatch({
type: "add",
text,
});
}
function handleEditTask(taskId, text) {
dispatch({
type: "edit",
id: taskId,
text,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: "delete",
id: taskId,
})
}
2. Ecrire la fonction de reducer
Commençons par créer un fichier tasksReducer.js. Ensuite il faut implémenter la fonction de reducer.
La fonction de reducer est la fonction qui va se charger de la logique. En fonction du type d’action demander, la fonction va appliquer un traitement sur le state.
Voici la structure de votre taskReducer :
function taskReducer(tasks, action) {
switch(action.type) {
case "add": {
return [...tasks, {id: tasks.length++, text: action.text}];
}
case "edit": {
return tasks.map(task => {
if (task.id === action.id) {
task.text = action.text;
}
return task;
});
}
case "delete": {
return tasks.filter((task) => task.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
Vous pouvez voir la fonction de reducer comme un aiguilleur de train. Chaque action passée à la fonction de reducer est analysé et redirigé vers le bon traitement à réaliser. Finalement le résultat est retourné.
3. Utiliser useReduce dans notre composant
Une fois que la fonction tasksReducer est implémentée, il reste à l’utiliser dans notre composant.
Il faut commencer par importer le hook useReducer.
import {useReducer} from 'react';
Ensuite, on change le state par le reducer comme suit :
// Remplacez state
const [tasks, setTasks] = useState(initialState);
// Par reducer
const [tasks, dispatch] = useReducer(tasks, initialState);
Bien sûr, on n’oublie pas d’importer le fichier tasksReducer.js.
Et voilà, votre composant n’embarque plus de logique pour la gestion du state, tout est géré dans le reducer ! Vous soumettez vos modifications à votre lead tech qui s’en voit ravit et valide votre PR.
Voici un schéma qui résume le fonctionnement du useReducer.
Quand utiliser un reducer ?
Pour savoir quand utiliser un reducer, il faut appliquer la règle des 3 (cf encadré). S’il y a plus de deux traitements différents à appliquer sur un state, alors vous pouvez partir sur un reducer. Vous prenez chaque cas pour lesquels vous utilisez un setState et analysez le traitement réalisé afin de donner un nom d’action à ce dernier.
En appliquant cette méthode, il vous sera facile d’extraire la logique du composant en l’exportant dans un reducer.
TLDR
Un reducer permet d’extraire la logique de gestion d’un state de composant en dehors de ce dernier afin de gagner en lisibilité. Il est également plus facile de réutiliser cette logique dans un autre composant sans dupliquer le code.
Un reducer (useReducer) est un hook React qui prend en paramètre une fonction yourReducer et la valeur initiale du state et expose une fonction dispatch et un state.
La fonction dispatch permet de dire au reducer l’action que l’on veut appliquer à notre state.
La fonction dispatch prend en paramètre un objet action qui est constitué d’un type qui explicite l’action a réaliser (ex : “edit_item”) ainsi que les données nécessaires à cette action.
La fonction de reducer yourReducer concentre toute la logique de mutation du state. Cette fonction prend le state courant ainsi que l’action à exécuter. La fonction est constituée d’un switch case faisant office d’aiguilleur pour appliquer les actions demandées. Attention, il est important de retourner le state modifié après le traitement !
L’objectif final du reducer est d’exposer un catalogue d’action à appliquer sur un state. Ainsi, on obtient des composants beaucoup moins complexes et plus lisibles. Le reducer devient le cerveau du composant, ordonner quelque chose et lui se charge de le faire !
Appliquer la règle des 3 pour savoir quand utiliser un reducer, c’est à dire, si vous avez au moins trois actions sur un state vous pouvez utiliser un reducer.