React LogoArchitecture front-end
Architecture/State management

React Context + useReducer

Jusqu'ici, on a géré nos états locaux avec useState. C'est parfait pour un compteur ou l'ouverture d'un menu. Mais quand une donnée doit être accessible partout dans l'application (l'utilisateur connecté, le thème, le panier), passer la donnée de parent en enfant (le prop drilling) devient impossible.

La solution native de React pour ça, c'est de combiner le Context API (pour distribuer la donnée) et useReducer (pour centraliser la logique de mise à jour).

Le fonctionnement de useReducer

useReducer est le grand frère de useState. On l'utilise quand l'état devient complexe (un objet avec plusieurs propriétés) et que la logique de mise à jour dépend de l'état précédent.

Au lieu de modifier l'état directement, on "dispatche" (envoie) une action. Un reducer (une fonction pure) intercepte cette action et retourne le nouvel état.

type CartItem = { id: string; name: string; price: number };

type State = { items: CartItem[]; total: number };
type Action = 
  | { type: 'ADD_ITEM'; payload: CartItem }
  | { type: 'CLEAR_CART' };

function cartReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'CLEAR_CART':
      return { items: [], total: 0 };
    default:
      return state;
  }
}

Dans le composant, on utilise le hook useReducer.

const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });

/* On modifie l'état en envoyant une action */
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>Vider</button>

Le piège du Context + Reducer

Pour rendre cet état global on doit créer un <CartContext.Provider> et l'entourer autour de notre application. C'est la méthode "historique" (fortement inspirée de Redux), mais elle souffre de gros défauts de maintenabilité :

  1. Le Boilerplate : Pour ajouter la moindre petite fonctionnalité, on doit modifier le type de l'action, rajouter un case dans le switch du reducer, modifier le contexte, etc.
  2. Le Provider Hell : Si on a 10 états globaux différents, notre App.tsx se retrouve encapsulé dans 10 Providers imbriqués.
  3. Le cauchemar des performances : Le Context API de React n'a pas de système de "sélecteur". Si le total du panier change, TOUS les composants qui consomment le CartContext vont se re-rendre, même s'ils n'avaient besoin que de la liste des items.

Exercice

L'idée c'est de reprendre le projet de panier e-commerce géré de manière "traditionnelle" avec useContext et useReducer.

Il faut ajouter une simple fonctionnalité : pouvoir supprimer un article spécifique du panier.

Vous devrez :

  • localiser le fichier contenant le reducer du panier
  • mettre à jour les types TypeScript pour autoriser une nouvelle action REMOVE_ITEM qui prendra l'ID du produit en payload
  • ajouter le case correspondant dans l'instruction switch du reducer
  • filtrer le tableau items sans muter l'état directement
  • câbler le bouton "Supprimer" dans l'item dans le sidebar