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.
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 actueldispatch: 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)
useReduceraide 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 à
itemset on augmentetotal. - 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.