Archives Wild Code School
React - Intermédiaire

React Hooks - useReducer

Quête 2 sur 7

Introduction

Selon la documentation de React,

"useReducer" est souvent préférable à useState quand vous avez une logique d’état local complexe qui comprend plusieurs sous-valeurs, ou quand l’état suivant dépend de l’état précédent."

"Reducer". Tu as peut-être déjà entendu ce terme. Ou peut-être était-ce "Reduce" ? Ou "Redux" ? Ce ne sont pas exactement les mêmes : "reduce" est une méthode disponible sur les tableaux (pour calculer des sommes, des moyennes, ...) et "Redux" est une bibliothèque de gestion globale d'états pour les applications JS (elle est indépendante de React).

Bien qu'elles ne soient pas identiques, useReducer, Redux et reduce sont très similaires dans leur fonctionnement. Ils font des choses étonnantes avec ce qu'on appelle des fonctionnalités de réduction.

Si tu connais déjà un peu Redux, il devrait être très facile de saisir les concepts derrière useReducer et si tu prévois d'apprendre Redux plus tard, apprendre useReducer (qui est un peu plus simple pour commencer) t'aidera beaucoup !

🤓 Ce que tu apprendras pendant cette quête :

✅ Comment créer un reducer ✅ Comment connecter un reducer à un composant React avec useReducer. ✅ Comment organiser ton code pour qu'il soit plus évolutif et maintenable à long terme

➡️ Reducers

Avant de commencer à plonger dans useReducer, voyons qu'est-ce qu'un reducer.

Un réducteur est une fonction pure qui prend comme paramètres :

  • Un état (chaîne de caractères, nombre, tableau, objet, ...)
  • une action (par convention, un objet avec une propriété "type" qui est une chaîne de caractères). Étant donné un état et un type d'action, il retournera un nouvel état.

Créons un reducer en pur Javascript:

codesandbox
https://codesandbox.io/embed/dazzling-heisenberg-qt4gf?fontsize=14&hidenavigation=1&theme=dark&view=editor]

Comme tu peux le voir, c'est une fonction vraiment simple, elle a deux entrées : l’état actuel et l'action que nous avons l'intention d'effectuer sur ce dernier) et une sortie : la nouvelle version de l'état.

Rien d'autre.

Ici, par convention, nous avons utilisé l'instruction switch pour décider quoi retourner en fonction du type d'action, mais nous aurions pu utiliser if/else, cela n'a pas vraiment d'importance tant que nous faisons la bonne mise à jour pour la bonne action.

Maintenant, ça pourrait être génial si on pouvait utiliser ce reducer pour gérer l’état d'un composant React.

Devine quoi ? C'est exactement le but de useReducer !


⚛️ useReducer

jsx
const [state, dispatch] = useReducer(reducer, initialState);

Voici useReducer.

Comme tu peux le voir, de manière similaire à useState, useReducer est une fonction qui retourne un tableau avec deux éléments (que nous stockons dans deux variables en utilisant la déstructuration de tableau)

  • Un état state qui prendra la valeur initiale spécifiée comme deuxième argument de la fonction
  • Une fonction qui permet de mettre indirectement à jour l'état par une action, qui est appelée dispatch par convention.

La différence avec useState c'est que dispatch appellera le reducer que nous devons fournir comme premier argument de useReducer.

Mettons cela en œuvre ! Nous pouvons garder le reducer que nous venons d'écrire :

codesandbox
https://codesandbox.io/embed/usereducer-firstexample-forked-nlb40?fontsize=14&hidenavigation=1&theme=dark

En fin de compte, useReducer est assez similaire à useState, mais au lieu de décrire directement comment l'état devrait changer, tu définis le comportement attendu pour chaque type d'action dans la fonction de réduction.

Pour réellement mettre à jour l'état et faire en sorte que ton composant re-renders, tu dois juste appeler dispatch avec la bonne action et laisser le réducteur s'occuper de la logique de mise à jour de l'état.

Comment cela va-t-il aider avec l'état complexe de l'application ?

Tu noteras que nous avons séparé l'intention (l'envoi d'une action) de la logique requise pour mettre à jour l'état (le reducer) !

Maintenant, notre composant React n'a pas à se soucier de la façon dont notre état doit être mis à jour. Il se contente de lier un événement (un clic sur un bouton dans ce cas) à une fonction qui exprime une intention donnée concernant l'état.

Maintenant, non seulement les choses sont plus explicites, mais le code est aussi plus facile à tester et plus réutilisable.

Imaginons que nous ayons un autre composant gérant un état similaire. Puisque nous avons extrait la logique de mise à jour de l’état dans un reducer, nous pouvons maintenant la réutiliser dans l'autre composant et ainsi éviter la répétition du code.

Bien sûr, pour faire les choses correctement, nous aurions besoin de déplacer le reducer dans un fichier séparé et d'importer ce dernier dans nos deux composants.

🆎 Types d'actions

C'est aussi une bonne pratique de créer un fichier séparé où l'on peut stocker la définition des actions.

Les types d'actions ne sont que des variables qui contiennent la définition des actions.

Pourquoi sont-ils utiles ?

Dans le cas où il y a une faute de frappe dans la chaîne de caractère, elle ira à l'instruction switch et lancera le cas "par défaut", donc nous ne pourrons pas facilement retracer l'origine du problème.

jsx
// ...
function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}
// ...
dispatch({type : "INCREMENT"});
// Will not throw any error even if there is a typo :/

Si nous créons une variable pour cela, il y aura une erreur car INCREMENT n'est pas un nom de variable défini.

jsx
// counter.actions.js
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

// counter.reducer.js
export function reducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
}

// App.js
dispatch({type : INCREMENT});
// ReferenceError: INCREMENT is not defined

"Logique d'état qui implique de multiples sous-valeurs"

Maintenant, imagine que nous voulions rendre notre compteur un peu plus compliqué et que nous laissons notre utilisateur décider du pas du compteur (de combien nous voulons augmenter ou diminuer le nombre).

Comment pouvons-nous faire cela ?

Nous pourrions faire de notre état initial un objet qui contient deux entrées :

jsx
// ...
const initialState = {
  counter: 0,
  step: 1
};
// ...

Bien sûr, il faudrait ajouter deux autres cas à notre reducer : un pour augmenter et un pour diminuer le pas. Le cas "incrémenter le pas" ressemblerait à ceci :

jsx
// ...
case INCREMENT_STEP:
      return { ...state, step: state.step + 1 };
// ...

Dans le cas où une action "INCREMENT_STEP" est envoyée, mettre à jour l’état en retournant un nouvel objet état (return { <SOMETHING> }) :

  • contenant toutes les paires clé/valeur dans l'objet état courant : ...state.
  • avec la valeur du pas actuel augmentée de un : step : state.step + 1.

Comme tu l'as peut-être deviné, les actions existantes dans le reducer doivent aussi tenir compte du fait que notre état est maintenant un objet qui contient le nombre et le pas, et pas seulement un nombre !

📚 Ressources

↗
React hooks documentation - useReducer

The official docs

reactjs.org
↗
Ben Awad - React Hooks useReducer Tutorial

A live demonstration of the classical (and over-used) counter-example, and another classical (and over-used) todo app example that shows how to make parameterized actions.

youtube.com

💪 Challenge

  • Fork ce CodeSandbox
  • Modifie initialState pour que soit un objet avec un attribut "counter" et "step" au lieu d'un simple nombre
  • Modifie l'application pour que la valeur de "counter" soit affichée et que le reducer fonctionne toujours
  • Ajoute deux cas dans le reducer pour gérer l'incrémentation/décrémentation du "step"
  • Ajoute deux boutons : "Incrémenter le pas" et "Décrémenter le pas" et les faire "expédier" l'action appropriée quand on clique dessus
  • Affiche la valeur du step (exactement comme la valeur du compteur)
  • Crée un fichier src/reducers/counterReducer.js et déplace le reducer. N'oublie ni l'export, ni l'import dans ton fichier App.js
  • Crée un fichier src/actions/counterActions.js et définis des constantes pour chaque type d'action. Export les et importe les dans Àpp.js puis utilise les.

🧐 Critères d'acceptation

  • [ ] On peut encore incrémenter et décrémenter le compteur (initialement de 1)
  • [ ] On peut changer le pas d'incrémentation/décrémentation et incrémenter/décrémenter le compteur en conséquence
  • [ ] Les constantes de définition des actions sont exportées depuis un fichier src/actions/counterActions.js.
  • [ ] Le reducer est exporté à partir d'un fichier src/reducers/counterReducer.js.
  • [ ] Le reducer utilise des constantes définies dans counterActions.js au lieu de chaînes de caractères brutes
  • [ ] L'application utilise des constantes définies dans counterActions.js au lieu de chaînes de caractères brutes

Ressources partagées sur cette quête