useReducer
useReducer
est un Hook React qui vous permet d’ajouter un réducteur à votre composant.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
- Référence
- Utilisation
- Dépannage
- J’ai dispatché une action, mais les journaux m’affichent l’ancienne valeur
- J’ai dispatché une action, mais l’écran ne se met pas à jour
- Une partie de l’état de mon réducteur devient undefined après le dispatch
- Tout l’état de mon réducteur devient undefined après le dispatch
- J’obtiens l’erreur « Too many re-renders »
- Mon réducteur ou ma fonction d’initialisation s’exécute deux fois
Référence
useReducer(reducer, initialArg, init?)
Appelez useReducer
au niveau racine de votre composant pour gérer son état avec un réducteur.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
Voir d’autres exemples ci-dessous.
Paramètres
reducer
: la fonction de réduction qui spécifie comment votre état est mis à jour. Elle doit être pure, prendre l’état et l’action en paramètres et renvoyer le prochain état. L’état et l’action peuvent être de n’importe quel type.initialArg
: la valeur à partir de laquelle l’état est calculé. Elle peut être de n’importe quel type. La façon dont l’état initial est calculé dépend du paramètre suivantinit
.init
optionnelle : la fonction d’initialisation qui doit renvoyer l’état initial. Si elle n’est pas spécifiée, l’état initial est défini avecinitialArg
. Autrement, il est défini en appelantinit(initialArg)
.
Valeur renvoyée
useReducer
renvoie un tableau avec exactement deux valeurs :
- L’état courant. Lors du premier rendu, il est défini avec
init(initialArg)
ouinitialArg
(s’il n’y a pas d’init
). - La fonction
dispatch
qui vous permet de mettre à jour l’état avec une valeur différente et ainsi redéclencher un rendu.
Limitations
useReducer
est un Hook, vous ne pouvez donc l’appeler qu’au niveau racine de votre composant ou dans vos propres Hooks. Vous ne pouvez pas l’appeler dans des boucles ou des conditions. Si vous avez besoin de le faire, extrayez un nouveau composant et déplacez-y l’état.- Dans le Mode Strict, React appellera deux fois votre réducteur et votre fonction d’initialisation afin de vous aider à trouver des impuretés accidentelles. Ce comportement est limité au développement et n’affecte pas la production. Si votre réducteur et votre fonction d’initialisation sont pures (comme ils devraient l’être), ça n’impactera pas votre logique. Le résultat de l’un des appels est ignoré.
Fonction dispatch
La fonction dispatch
renvoyée par useReducer
vous permet de mettre à jour l’état avec une valeur différente et de déclencher un rendu. Le seul paramètre à passer à la fonction dispatch
est l’action :
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React définira le prochain état avec le résultat de l’appel de la fonction reducer
que vous avez fournie avec le state
courant et l’action que vous avez passée à dispatch
.
Paramètres
action
: l’action réalisée par l’utilisateur. Elle peut être de n’importe quelle nature. Par convention, une action est généralement un objet avec une propriététype
permettant son identification et, optionnellement, d’autres propriétés avec des informations additionnelles.
Valeur renvoyée
Les fonctions dispatch
ne renvoient rien.
Limitations
-
La fonction
dispatch
ne met à jour l’état que pour le prochain rendu. Si vous lisez une variable d’état après avoir appelé la fonction dedispatch
, vous aurez encore l’ancienne valeur qui était à l’écran avant cet appel. -
Si la nouvelle valeur fournie est identique au
state
actuel, déterminé par une comparaison avecObject.is
, React sautera le nouveau rendu du composant et de ses enfants. C’est une optimisation. React peut toujours appeler votre composant avant d’en ignorer le résultat, mais ça ne devrait pas affecter votre code. -
React met à jour l’état par lots. Il met à jour l’écran une fois que tous les gestionnaires d’événement se sont exécutés et ont appelé leurs fonctions
set
. Ça évite les rendus multiples à la suite d’un événement unique. Dans les rares cas où vous devez forcer React à mettre à jour l’écran prématurément, par exemple pour accéder au DOM, vous pouvez utiliserflushSync
.
Utilisation
Ajouter un réducteur à un composant
Appelez useReducer
au niveau racine de votre composant pour gérer l’état avec un réducteur.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
renvoie un tableau avec exactement deux éléments :
- L’état actuel de cette variable d’état, défini initialement avec l’état initial que vous avez fourni.
- La fonction
dispatch
qui vous permet de le modifier en réponse à une interaction.
Pour mettre à jour ce qui est à l’écran, appelez dispatch
avec un objet, nommé action, qui représente ce que l’utilisateur a fait :
function handleClick() {
dispatch({ type: 'incremented_age' });
}
React passera l’état actuel et l’action à votre fonction de réduction. Votre réducteur calculera et renverra le nouvel état. React sauvera ce nouvel état, fera le rendu avec celui-ci puis mettra à jour l’interface utilisateur.
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Unknown action.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Incrémenter l'âge </button> <p>Bonjour ! Vous avez {state.age} ans.</p> </> ); }
useReducer
est très similaire à useState
, mais il vous permet de déplacer la logique de mise à jour de l’état des gestionnaires d’événements vers une seule fonction à l’extérieur de votre composant. Apprenez-en davantage pour choisir entre useState
et useReducer
.
Écrire la fonction de réduction
Une fonction de réduction est déclarée ainsi :
function reducer(state, action) {
// ...
}
Ensuite, vous devez la remplir avec le code qui va calculer et renvoyer le prochain état. Par convention, il est courant de l’écrire en utilisant une instruction switch
. Pour chaque case
du switch
, calculez et renvoyez un état suivant.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
Les actions peuvent prendre n’importe quelle forme. Par convention, il est courant de passer des objets avec une propriété type
pour identifier l’action. Ils doivent juste inclure les informations nécessaires au réducteur pour calculer le prochain état.
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Clara', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...
Les noms des types d’actions sont locales à votre composant. Chaque action décrit une seule interaction, même si ça amène à modifier plusieurs fois la donnée. La forme de l’état est arbitraire, mais ce sera généralement un objet ou un tableau.
Lisez Extraire la logique d’état dans un réducteur pour en apprendre davantage.
Exemple 1 sur 3 · Formulaire (objet)
Dans cet exemple, le réducteur gère un état sous forme d’objet ayant deux champs : name
et age
.
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Unknown action: ' + action.type); } const initialState = { name: 'Clara', age: 42 }; export default function Form() { const [state, dispatch] = useReducer(reducer, initialState); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } return ( <> <input value={state.name} onChange={handleInputChange} /> <button onClick={handleButtonClick}> Incrémenter l'âge </button> <p>Bonjour, {state.name}. Vous avez {state.age} ans.</p> </> ); }
Éviter de recréer l’état initial
React sauvegarde l’état initial une fois et l’ignore lors des rendus suivants.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
Bien que le résultat de createInitialState(username)
soit seulement utilisé pour le premier rendu, vous continuez d’appeler cette fonction à chaque rendu. C’est du gâchis si elle crée de grands tableaux ou effectue des calculs gourmands.
Pour corriger ça, vous pouvez plutôt la passer comme fonction d’initialisation au useReducer
en tant que troisième argument.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
Remarquez que vous passez createInitialState
, qui est elle-même une fonction, et non createInitialState()
, qui est le résultat de son exécution. De cette façon, l’état initial n’est pas recréé après l’initialisation.
Dans l’exemple ci-dessus, createInitialState
prend un argument username
. Si votre fonction d’initialisation n’a besoin d’aucune information pour calculer l’état initial, vous pouvez passez null
comme second argument à useReducer
.
Exemple 1 sur 2 · Passer la fonction d’initialisation
Cet exemple passe la fonction d’initialisation, la fonction createInitialState
ne s’exécute que durant l’initialisation. Elle ne s’exécute pas lorsque le composant fait de nouveau son rendu, comme lorsque vous tapez dans le champ de saisie.
import { useReducer } from 'react'; function createInitialState(username) { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: "Tâche #" + (i + 1) + " de " + username }); } return { draft: '', todos: initialTodos, }; } function reducer(state, action) { switch (action.type) { case 'changed_draft': { return { draft: action.nextDraft, todos: state.todos, }; }; case 'added_todo': { return { draft: '', todos: [{ id: state.todos.length, text: state.draft }, ...state.todos] } } } throw Error('Unknown action: ' + action.type); } export default function TodoList({ username }) { const [state, dispatch] = useReducer( reducer, username, createInitialState ); return ( <> <input value={state.draft} onChange={e => { dispatch({ type: 'changed_draft', nextDraft: e.target.value }) }} /> <button onClick={() => { dispatch({ type: 'added_todo' }); }}>Ajouter</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
Dépannage
J’ai dispatché une action, mais les journaux m’affichent l’ancienne valeur
Appeler la fonction dispatch
ne change pas l’état dans le code qui est en train de s’exécuter :
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Demande un nouveau rendu avec 43
console.log(state.age); // Toujours 42 !
setTimeout(() => {
console.log(state.age); // 42 ici aussi !
}, 5000);
}
C’est parce que l’état se comporte comme un instantané. Mettre à jour un état nécessite un nouveau rendu avec sa nouvelle valeur, mais n’affecte pas la variable JavaScript state
dans votre gestionnaire d’événement qui est en cours d’exécution.
Si vous avez besoin de deviner la prochaine valeur de l’état, vous pouvez la calculer en appelant vous-même votre réducteur :
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
J’ai dispatché une action, mais l’écran ne se met pas à jour
React ignorera votre mise à jour si le prochain état est égal à l’état précédent, déterminé avec une comparaison Object.is
. C’est généralement ce qui arrive quand vous changez l’objet ou le tableau directement dans l’état :
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Incorrect : modification de l'objet existant
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Incorrect : modification de l'objet existant
state.name = action.nextName;
return state;
}
// ...
}
}
Vous avez modifié un objet existant de state
puis l’avez renvoyé, React a ainsi ignoré la mise à jour. Pour corriger ça, vous devez toujours vous assurer que vous mettez à jour les objets dans l’état et mettez à jour les tableaux dans l’état plutôt que de les modifier :
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correct : création d'un nouvel objet
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correct : création d'un nouvel objet
return {
...state,
name: action.nextName
};
}
// ...
}
}
Une partie de l’état de mon réducteur devient undefined après le dispatch
Assurez-vous que chaque branche case
copie tous les champs existants lorsqu’il renvoie le nouvel état :
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // N'oubliez pas ceci !
age: state.age + 1
};
}
// ...
Sans le ...state
ci-dessus, le prochain état renvoyé ne contiendrait que le champ age
et rien d’autre.
Tout l’état de mon réducteur devient undefined après le dispatch
Si votre état devient undefined
de manière imprévue, c’est que vous avez probablement oublié de return
l’état dans l’un de vos cas, ou que le type d’action ne correspond à aucune des instructions case
. Pour comprendre pourquoi, lancez une erreur à l’extérieur du switch
:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
Vous pouvez également utiliser un vérificateur de type statique comme TypeScript pour détecter ces erreurs.
J’obtiens l’erreur « Too many re-renders »
Vous pouvez rencontrer l’erreur indiquant Too many re-renders. React limits the number of renders to prevent an infinite loop.
(Trop de nouveaux rendus. React limite le nombre de rendus pour éviter des boucles infinies, NdT). Ça signifie généralement que vous dispatchez une action inconditionnellement pendant un rendu et ainsi votre composant entre dans une boucle : rendu, dispatch (qui occasionne un rendu), rendu, dispatch (qui occasionne un rendu), et ainsi de suite. La cause en est très souvent une erreur dans la spécification d’un gestionnaire d’événement :
// 🚩 Incorrect : appelle le gestionnaire pendant le rendu
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct : passe le gestionnaire d'événement
return <button onClick={handleClick}>Click me</button>
// ✅ Correct : passe une fonction en ligne
return <button onClick={(e) => handleClick(e)}>Click me</button>
Si vous ne trouvez pas la cause de cette erreur, cliquez sur la flèche à côté de l’erreur dans la console et parcourez la pile JavaScript pour trouver l’appel à la fonction dispatch
responsable de l’erreur.
Mon réducteur ou ma fonction d’initialisation s’exécute deux fois
Dans le Mode Strict, React appellera votre réducteur et votre fonction d’initialisation deux fois. Ça ne devrait pas casser votre code.
Ce comportement spécifique au développement vous aide à garder les composants purs. React utilise le résultat de l’un des appels et ignore l’autre. Tant que votre composant, votre fonction d’initialisation et votre réducteur sont purs, ça ne devrait pas affecter votre logique. Si toutefois ils sont malencontreusement impurs, ça vous permettra de détecter les erreurs.
Par exemple, cette fonction de réduction impure modifie un tableau dans l’état :
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Erreur : modification de l'état
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
Comme React appelle deux fois votre fonction de réduction, vous verrez que la liste a été ajoutée deux fois, vous saurez donc qu’il y a un problème. Dans cet exemple, vous pouvez le corriger en remplaçant le tableau plutôt qu’en le modifiant :
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correct : remplacer avec un nouvel état
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}
Maintenant que cette fonction de réduction est pure, l’appeler une fois de plus ne change pas le comportement. C’est pourquoi React vous aide à trouver l’erreur en l’appelant deux fois. Seuls les composants, les fonctions d’initialisations et les réducteurs doivent être purs. Les gestionnaires d’événements n’ont pas besoin de l’être, React ne les appellera donc jamais deux fois.
Lisez Garder les composants purs pour en apprendre davantage.