Contactez-nous

`useReducer` - Gestion d'état complexe

Découvrez le hook useReducer de React, une alternative à useState pour gérer des logiques d'état complexes de manière prévisible et structurée (inspiré de Redux).

Introduction : Quand `useState` atteint ses limites

Le hook `useState` est parfait pour gérer des états simples dans les composants React : un booléen, une chaîne, un nombre, voire un objet ou un tableau simple. Cependant, à mesure que la logique d'état d'un composant devient plus complexe, l'utilisation de multiples `useState` peut devenir difficile à gérer.

Imaginez un composant dont l'état implique plusieurs sous-valeurs interdépendantes, ou dont les transitions d'état sont complexes et dépendent fortement de l'état précédent et d'une action spécifique. Gérer tout cela avec plusieurs `useState` peut entraîner :

  • Une logique de mise à jour éparpillée dans plusieurs gestionnaires d'événements.
  • Des difficultés à suivre comment l'état évolue.
  • Une augmentation du risque d'erreurs lors des mises à jour complexes.
  • Une testabilité réduite de la logique d'état.

Pour ces scénarios, React propose une alternative plus robuste et structurée : le hook `useReducer`. Il est particulièrement bien adapté lorsque la logique de mise à jour de l'état est complexe ou lorsque le prochain état dépend de l'état précédent de manière non triviale.

Le concept : Inspiré par Redux (Reducer Pattern)

Si vous êtes familier avec Redux, le concept de `useReducer` vous semblera très proche. Il est basé sur le "Reducer Pattern" qui centralise toute la logique de modification de l'état dans une seule fonction appelée reducer.

Les éléments clés sont :

  1. State : La valeur actuelle de l'état, similaire à ce que vous stockeriez avec `useState`.
  2. Action : Un objet décrivant le type de modification que vous souhaitez effectuer sur l'état. Il contient généralement une propriété `type` (une chaîne identifiant l'action) et parfois une propriété `payload` (contenant les données nécessaires à la mise à jour).
  3. Reducer : Une fonction pure qui prend l'état actuel et une action en arguments, et qui retourne le nouvel état. Toute la logique de transition d'état est encapsulée ici. (currentState, action) => newState.
  4. Dispatch : Une fonction spéciale fournie par `useReducer` que vous appelez pour "envoyer" (dispatch) une action à votre reducer. C'est le seul moyen de déclencher une mise à jour de l'état.

L'idée est donc de séparer clairement "ce qui s'est passé" (l'action) de "comment l'état change en réponse" (le reducer).

Mise en place de `useReducer`

L'utilisation de `useReducer` se fait en trois étapes principales : définir le reducer, définir l'état initial, et appeler le hook.

1. Définir la fonction Reducer :

C'est une fonction pure qui reçoit l'état actuel et une action, et retourne le nouvel état. Elle contient typiquement une instruction `switch` basée sur le `action.type`.

// Exemple : Reducer pour un compteur
function counterReducer(state, action) {
  console.log('Reducer appelé avec state:', state, 'et action:', action);
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    case 'SET_VALUE':
      // Utilise le payload pour définir une valeur spécifique
      return { count: action.payload }; 
    default:
      // Important: retourner l'état actuel si l'action n'est pas reconnue
      // ou lancer une erreur
      // throw new Error(`Action non supportée: ${action.type}`);
      return state;
  }
}
2. Définir l'état initial :

C'est la valeur que l'état aura lors du premier rendu.

const initialState = { count: 0 };
3. Appeler `useReducer` dans le composant :

Le hook prend le reducer et l'état initial en arguments. Il retourne un tableau contenant l'état actuel et la fonction `dispatch`.

import React, { useReducer } from 'react';

// ... (définition de counterReducer et initialState ci-dessus)

function CounterWithReducer() {
  // Appel du hook
  const [state, dispatch] = useReducer(counterReducer, initialState);

  // Utilisation de 'state' pour l'affichage
  // Utilisation de 'dispatch' pour déclencher les mises à jour
  return (
    

Compteur (useReducer)

Compte : {state.count}

{/* Appel de dispatch avec des objets 'action' */}
); } export default CounterWithReducer;

Lorsque vous cliquez sur un bouton, la fonction `dispatch` est appelée avec un objet action. React passe alors l'état actuel et cette action à votre `counterReducer`. Le reducer calcule et retourne le nouvel état. React stocke ce nouvel état et planifie un re-rendu du composant avec la nouvelle valeur de `state`.

Actions et Payloads

Les actions sont de simples objets JavaScript qui servent de messages pour décrire un changement souhaité. La convention la plus répandue est d'avoir :

  • Une propriété type : Une chaîne (souvent en majuscules avec des underscores) qui identifie le type d'action (ex: 'ADD_TODO', 'SET_LOADING_STATUS'). C'est ce que le reducer utilise dans son switch.
  • Une propriété payload (optionnelle) : Contient les données supplémentaires nécessaires pour effectuer la mise à jour (ex: le texte d'une nouvelle tâche, la nouvelle valeur d'un compteur, l'indicateur de chargement).

Cette structure rend les intentions de mise à jour très explicites.

Avantages par rapport à `useState`

  • Logique d'état centralisée : Toute la logique de transition d'état est regroupée dans la fonction reducer, rendant le code plus facile à comprendre, à maintenir et à déboguer par rapport à une logique éparpillée dans plusieurs `setState` au sein de divers gestionnaires d'événements.
  • Testabilité améliorée : La fonction reducer est une fonction pure (elle ne dépend que de ses arguments et n'a pas d'effets de bord). Elle peut donc être testée très facilement de manière isolée, sans avoir besoin de rendre le composant React.
  • Prévisibilité : Les transitions d'état deviennent plus prévisibles car elles passent toutes par le même point central (le reducer) en réponse à des actions explicites.
  • Gestion de la complexité : Particulièrement efficace lorsque le prochain état dépend de manière complexe de l'état précédent et de l'action effectuée.
  • Optimisation avec Context : Si vous passez la fonction `dispatch` via l'API Context à des composants enfants, React garantit que la référence de `dispatch` est stable entre les rendus. Cela signifie que les composants enfants qui ne reçoivent que `dispatch` (et pas l'état) n'auront pas besoin d'être re-rendus inutilement si seul l'état change (à condition d'utiliser `React.memo` ou des optimisations similaires sur ces enfants). C'est plus difficile à réaliser avec les fonctions `setState` de `useState` qui peuvent changer si elles capturent des valeurs de props ou d'état.

Conclusion : Un outil puissant pour la structure et la prévisibilité

`useReducer` est un hook puissant qui brille lorsque la complexité de la gestion d'état d'un composant dépasse ce que `useState` peut gérer élégamment. En adoptant le pattern Reducer, il encourage une séparation claire entre la description des changements (actions) et la logique de mise à jour (reducer), menant à un code plus structuré, prévisible et testable.

Bien qu'il introduise un peu plus de boilerplate initial que `useState`, les bénéfices en termes de maintenabilité et de clarté pour les états complexes en font un ajout essentiel à la boîte à outils de tout développeur React. Il constitue également une excellente introduction aux concepts fondamentaux utilisés dans les bibliothèques de gestion d'état plus globales comme Redux.