Tutoriel React — Chapitre 18

Context API (state global)

Chapitre 18 — Context API (state global)

Quand votre state devient plus “structuré” (plusieurs champs liés, plusieurs actions possibles), useState peut vite devenir difficile à lire. Le hook useReducer propose une approche plus organisée : vous décrivez des actions et un reducer qui calcule le prochain state.

Objectif : comprendre (pas apprendre par cœur) Niveau : débutant absolu Pratique : mini-exercice à la fin

Plan du chapitre

1) Quand utiliser useReducer ?

useReducer est utile quand :

  • le state contient plusieurs valeurs liées (ex : panier : items, total, promo…)
  • il y a plusieurs actions possibles (ajouter, supprimer, vider, appliquer une réduction…)
  • vous voulez centraliser les règles de mise à jour dans un seul endroit

Si vous avez 1 ou 2 valeurs simples : useState suffit. Si le state ressemble à un “mini-système” : useReducer devient plus propre.

2) L’idée : actions + reducer

Un reducer est une fonction qui reçoit :

  • le state actuel
  • une action (un objet qui décrit ce que l’on veut faire)

et qui retourne :

  • le nouveau state
function reducer(state, action) {
  // action = { type: "INCREMENT" } par exemple
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    default:
      return state;
  }
}

Règle : un reducer doit être “pur” (sans fetch, sans timers, sans mutation). Il calcule uniquement un nouveau state.

3) Syntaxe de useReducer

useReducer retourne :

  • state : l’état actuel
  • dispatch : fonction pour envoyer une action
import { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

export default function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button>
    </div>
  );
}

4) Exemple 1 : compteur avec actions (plus complet)

On ajoute une action avec “payload” (valeur envoyée) : incrémenter de 5 par exemple.

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "ADD":
      return { ...state, count: state.count + action.payload };
    case "RESET":
      return initialState;
    default:
      return state;
  }
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Compteur : {state.count}</p>

      <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button>
      <button onClick={() => dispatch({ type: "ADD", payload: 5 })}>+5</button>
      <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
    </div>
  );
}

Le “payload” sert quand une action a besoin d’une information (ici, combien ajouter).

5) Exemple 2 : formulaire avec reducer

Un formulaire peut avoir plusieurs champs, des erreurs, un statut “loading”… useReducer est pratique pour centraliser.

A) State initial

const initialState = {
  form: { firstname: "", email: "" },
  errors: {},
  submitted: false
};

B) Reducer

function reducer(state, action) {
  switch (action.type) {
    case "CHANGE_FIELD": {
      const { name, value } = action.payload;
      return {
        ...state,
        form: { ...state.form, [name]: value },
        submitted: false
      };
    }

    case "SET_ERRORS":
      return { ...state, errors: action.payload };

    case "SUBMIT_OK":
      return { ...state, errors: {}, submitted: true };

    case "RESET":
      return initialState;

    default:
      return state;
  }
}

C) Utilisation

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

const handleChange = (e) => {
  dispatch({
    type: "CHANGE_FIELD",
    payload: { name: e.target.name, value: e.target.value }
  });
};

L’idée : toutes les modifications passent par dispatch. Le reducer devient l’unique endroit où l’on définit “comment le state change”.

6) Bonnes pratiques

  • Utilisez des types d’actions explicites : "ADD_ITEM", "REMOVE_ITEM", etc.
  • Gardez le reducer “pur” : pas d’API, pas de side effects.
  • Ne modifiez jamais le state directement : toujours des copies (...).
  • Si le reducer devient énorme, découpez en plusieurs reducers (niveau plus avancé).

7) Erreurs fréquentes

Je modifie directement le state (mutation)

Exemple interdit : state.count++. Un reducer doit retourner un nouvel objet, pas modifier l’ancien.

Je fais un fetch dans le reducer

Mauvaise pratique. Les effets doivent rester dans useEffect. Le reducer doit uniquement calculer le prochain state.

Je ne comprends pas “dispatch”

dispatch signifie “envoyer une action”. Vous décrivez ce que vous voulez : { type: "RESET" }. Le reducer applique la règle correspondante.

8) Résumé (à retenir)

  • useReducer aide quand le state devient complexe.
  • On envoie des actions avec dispatch.
  • Le reducer reçoit (state, action) et retourne un nouveau state.
  • Le reducer doit rester pur et sans mutation.

9) Exercice pratique

Créez un mini “panier” avec useReducer :

  • State : items (liste), total (nombre)
  • Actions : ADD_ITEM, REMOVE_ITEM, CLEAR

Contraintes

  • Un item : { id, name, price }
  • Quand on ajoute un item, on l’ajoute à items et on augmente total.
  • Quand on supprime, on retire l’item et on diminue total.
  • “CLEAR” vide tout.

Prochaine étape : Chapitre 19 — Mémoïsation (useMemo / useCallback) pour optimiser et éviter des re-renders inutiles.