Kodokon kodokon.com

useReducer pour l'état complexe

Structurez les états complexes avec un reducer pur qui centralise les transitions et élimine les combinaisons impossibles.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

Dès qu'un composant orchestre plusieurs valeurs d'état qui évoluent ensemble - un statut de requête, des données, une erreur -, multiplier les useState disperse la logique de transition dans chaque gestionnaire d'événements. Vous connaissez le symptôme : trois setters à appeler dans le bon ordre, et tôt ou tard un état incohérent, comme un statut à success accompagné d'une erreur non nulle. useReducer inverse la responsabilité : l'état devient un objet unique, et chaque changement passe par une action interprétée par une fonction centrale, le reducer.

JAVASCRIPT
function requestReducer(state, action) {
  switch (action.type) {
    case "start":
      return { status: "loading", data: null, error: null };
    case "resolve":
      return {
        status: "success",
        data: action.data,
        error: null,
      };
    case "reject":
      return {
        status: "error",
        data: null,
        error: action.error,
      };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}
Chaque transition reconstruit un état complet et cohérent.

Le reducer est une fonction pure : il reçoit l'état courant et une action, puis retourne le prochain état, sans effet de bord. Les bénéfices sont immédiats : les transitions sont explicites et exhaustives, les états impossibles disparaissent à la source (chaque case reconstruit un objet cohérent), et la logique se lit en un seul endroit au lieu d'être éparpillée. Signal d'alarme utile : si vous copiez-collez la même séquence de setState à deux endroits, c'est probablement un reducer qui manque.

JSX
function UserProfile() {
  const [state, dispatch] = useReducer(requestReducer, {
    status: "idle",
    data: null,
    error: null,
  });

  async function load(id) {
    dispatch({ type: "start" });
    try {
      const res = await fetch("/api/users/" + id);
      const data = await res.json();
      dispatch({ type: "resolve", data });
    } catch (error) {
      dispatch({ type: "reject", error });
    }
  }

  return <button onClick={() => load(1)}>Charger</button>;
}
Le composant exprime l'intention, le reducer décide de l'état.

Le choix entre useState et useReducer n'est pas une affaire de goût. Gardez useState pour des valeurs simples et indépendantes ; passez à useReducer quand le prochain état dépend du précédent ou quand plusieurs champs changent de concert. Deux atouts supplémentaires : le troisième argument offre une initialisation paresseuse (useReducer(reducer, arg, init) n'exécute init qu'au premier rendu), et React garantit que dispatch conserve la même identité d'un rendu à l'autre - vous pouvez le passer aux enfants sans useCallback.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Quel signal doit vous orienter vers useReducer plutôt que vers plusieurs useState ?
    • Le composant dépasse une centaine de lignes
    • Plusieurs valeurs d'état changent ensemble et le nouvel état dépend du précédent
    • Le composant reçoit plus de trois props
    • L'état contient un tableau
  2. Un reducer retourne l'objet d'état reçu, muté en place. Que fait React ?
    • Il compare l'ancienne et la nouvelle référence, les trouve égales et saute le re-rendu
    • Il détecte la mutation en profondeur et re-rend le composant
    • Il lève une erreur car l'état est gelé en développement
  3. Pourquoi peut-on passer dispatch à un enfant mémoïsé sans précaution particulière ?
    • Parce que React.memo ignore les props de type fonction
    • Parce que dispatch est recréé mais structurellement égal à chaque rendu
    • Parce que React garantit que dispatch conserve la même identité pendant toute la vie du composant