useReducer est un hook React qui garde l'état comme useState, mais regroupe toutes les façons de le modifier dans une seule fonction, le reducer. Votre composant appelle dispatch avec un objet action comme { type: 'increment' }, et React passe l'état actuel et cette action au reducer, qui renvoie l'état suivant.
Les boutons ne disent pas comment le compteur change, seulement ce qui s'est passé. Tout le calcul vit dans reducer. Ajoutez un case 'double' qui renvoie { count: state.count * 2 } et un bouton qui l'envoie pour voir comment s'intègre un nouveau type de mise à jour.
La syntaxe
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reducerest une fonction(state, action) => nextState. Définissez-la hors du composant ; elle n'a besoin de rien dans la portée du composant.initialArgest l'état de départ, utilisé au premier rendu seulement.initest facultatif. Si vous le passez, l'état de départ estinit(initialArg)à la place.stateest l'état actuel pour ce rendu.dispatch(action)envoie une action au reducer et planifie un rendu avec le résultat. Il est stable : c'est la même fonction à chaque rendu, vous pouvez donc la passer vers le bas ou la lister comme dépendance sans risque.
Une action peut être n'importe quelle valeur, mais par convention c'est un objet avec une chaîne type qui décrit ce qui s'est passé, plus les données dont le reducer a besoin : { type: 'added', text: 'Buy milk' }.
Écrire un reducer
La plupart des reducers sont un switch sur action.type, avec un case par type d'événement. Chaque case renvoie un état entièrement nouveau ; il ne modifie jamais l'ancien. Le case default lève une erreur, pour qu'une faute de frappe comme dispatch({ type: 'incremnet' }) échoue bruyamment au lieu de ne rien faire.
Nommez les actions d'après ce que l'utilisateur a fait (added, toggled, deleted), pas d'après le changement d'état que vous avez en tête (setTodos). Le composant se lit alors comme une liste d'événements, et le reducer est le seul endroit qui décide de ce que signifie chaque événement.
Les reducers doivent être purs
Un reducer est une fonction pure : pour le même état et la même action, il renvoie le même résultat, et ne fait rien d'autre. Donc pas de mutation, pas de requêtes, pas de minuteurs, pas de Math.random() ni de Date.now() à l'intérieur. React s'appuie sur cela. En développement sous StrictMode, React appelle votre reducer deux fois pour chaque action et garde un seul résultat, pour vous aider à repérer un reducer qui n'est pas pur. L'aperçu fonctionne ici comme un build de production, les exemples de cette page l'appellent donc une seule fois.
La règle sur la mutation est celle qu'on enfreint le plus. Pousser dans state.todos et renvoyer state renvoie le même objet, donc React ne voit aucun changement et saute le rendu, le même piège que celui décrit dans mettre à jour tableaux et objets. Construisez de nouveaux tableaux et objets avec le spread, map et filter.
// Wrong: mutates and returns the same object
case 'added':
state.todos.push(action.todo);
return state;
// Right: returns a new object with a new array
case 'added':
return { ...state, todos: [...state.todos, action.todo] };
Les effets de bord qui appartiennent à un événement, comme une sauvegarde sur un serveur, vont dans le gestionnaire d'événement à côté de dispatch. Les effets de bord qui appartiennent à l'état, comme sa synchronisation avec localStorage, vont dans un effet.
Une liste de tâches avec useReducer
Voici le reducer qui fait un vrai travail : ajouter, cocher et supprimer des tâches. Le console.log en haut du reducer affiche chaque action, un moyen pratique de suivre l'évolution de l'état pendant le débogage. C'est le seul effet de bord couramment toléré dans un reducer, car il ne modifie rien ; retirez-le avant la mise en production.
Ajoutez une tâche, cochez une case et supprimez-en une, puis lisez la console : chaque changement est une ligne qui nomme l'action et ses données. Le texte du champ reste dans useState, car il est local au champ et aucun autre événement n'y touche. Combiner les deux hooks dans un composant est normal.
L'identifiant de la nouvelle tâche est créé dans le gestionnaire de clic et voyage dans l'action, le reducer ne fait donc que le copier. Écrire id: nextId++ dans le reducer le rendrait impur : sous StrictMode en développement, le second appel sauterait un identifiant.
dispatch ne change pas l'état tout de suite
dispatch fonctionne comme un setter de useState : il planifie un rendu, et la variable state de votre gestionnaire actuel garde l'ancienne valeur. Pour utiliser le nouvel état tout de suite, calculez-le vous-même en appelant le reducer.
Cliquez plusieurs fois. La première ligne affichée a toujours un pas de retard, et la seconde correspond au nombre sur le bouton après le rendu. Comme le reducer est une simple fonction, l'appeler vous-même est sans risque, un autre avantage de le garder pur.
useState ou useReducer
Les deux hooks stockent de l'état, et tout ce que vous écrivez avec l'un peut s'écrire avec l'autre. La différence tient à l'endroit où vit la logique de mise à jour.
| useState | useReducer | |
|---|---|---|
| Logique de mise à jour | Dans chaque gestionnaire d'événement | Dans une seule fonction reducer |
| Adapté à | Quelques valeurs indépendantes | Plusieurs champs modifiés ensemble par de nombreux événements |
| Quantité de code | Moins pour un état simple | Plus au départ, moins quand les événements se multiplient |
| Tests | Tester via le composant | Tester le reducer comme une simple fonction |
| Débogage | Trouver quel gestionnaire l'a modifié | Afficher chaque action au même endroit |
Passez à useReducer quand vous remarquez que le même état est mis à jour dans de nombreux gestionnaires, quand un événement doit modifier plusieurs éléments d'état qui doivent rester cohérents, ou quand un gestionnaire contient surtout de la logique sur l'état suivant. Pour un interrupteur ou un champ texte, useState est plus court et plus clair. Changer plus tard n'est pas difficile : remplacez les setters par des dispatches et déplacez la logique dans des cases.
Initialisation paresseuse
Si vous écrivez l'état initial comme un appel de fonction, par exemple useReducer(reducer, createInitialState('Ada')), cet appel s'exécute à chaque rendu, même si React n'utilise son résultat que la première fois. Si construire l'état initial coûte cher, ou doit être dérivé d'une prop, passez un troisième argument : une fonction init. React appelle init(initialArg) une seule fois, au premier rendu.
Tapez dans le champ : le composant refait un rendu à chaque frappe, mais createInitialState n'affiche qu'une seule fois. Remplacez maintenant l'appel par useReducer(reducer, createInitialState('Ada')) et tapez à nouveau. La fonction s'exécute à chaque rendu, et React jette le résultat à chaque fois après le premier.
Passez la fonction elle-même, pas le résultat de son appel. useReducer(reducer, 'Ada', createInitialState) est paresseux ; useReducer(reducer, createInitialState('Ada')) ne l'est pas.
useReducer avec le contexte
Un reducer s'associe bien au contexte quand un arbre profond a besoin de l'état. Placez l'état et dispatch dans un contexte en haut de l'arbre, et n'importe quel composant en dessous peut lire l'état ou envoyer des actions sans props transmises à travers chaque couche. Comme dispatch ne change jamais, les composants qui ne font qu'envoyer des actions peuvent lire un contexte séparé pour dispatch et éviter un nouveau rendu quand l'état change.
import { createContext, useContext, useReducer } from 'react';
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
export function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}
Dans React 19, un objet contexte sert de provider à lui seul (<TodosContext value={...}>) ; <TodosContext.Provider> fonctionne toujours aussi. La page sur useContext explique comment le contexte atteint les composants et quand il leur fait refaire un rendu.
Questions fréquentes
Qu'est-ce que useReducer dans React ?
Un hook pour un état dont les mises à jour sont décrites par des actions. Vous appelez const [state, dispatch] = useReducer(reducer, initialState), puis dispatch({ type: 'added' }) depuis les gestionnaires d'événements. React passe l'état actuel et l'action à votre reducer, et ce qu'il renvoie devient l'état suivant.
Quand utiliser useReducer plutôt que useState ?
Quand plusieurs événements mettent à jour le même état de façons liées, quand l'état suivant dépend de plusieurs champs à la fois, ou quand la logique de mise à jour est assez longue pour mériter une fonction testable hors du composant. Pour une ou deux valeurs indépendantes, useState est plus simple.
Pourquoi un reducer doit-il être pur ?
React peut appeler votre reducer plus d'une fois pour la même action (StrictMode le fait exprès en développement) et attend le même résultat à chaque fois. Un reducer ne doit donc pas muter l'état, récupérer des données, lancer des minuteurs ni lire des valeurs aléatoires. Faites cela dans les gestionnaires d'événements ou les effets.
dispatch met-il à jour l'état immédiatement ?
Non. Comme un setter de useState, dispatch planifie un rendu. Dans le gestionnaire d'événement en cours, state contient toujours l'ancienne valeur ; le nouvel état apparaît au rendu suivant.
Quel est le troisième argument de useReducer ?
Une fonction init facultative. Si vous la passez, React calcule l'état initial avec init(initialArg), et seulement au premier rendu. C'est utile quand construire l'état initial coûte cher ou dépend d'une prop.