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

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 quels types.
  • 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 init qui suit.
  • 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 avec initialArg. Autrement, il est défini en appelant init(initialArg).

Valeur renvoyée

useReducer renvoie un tableau avec exactement deux valeurs :

  1. L’état courant. Lors du premier rendu, il est défini avec init(initialArg) ou initialArg (s’il n’y a pas d’argument init).
  2. La fonction dispatch qui vous permet de mettre à jour l’état vers 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.
  • En Mode Strict, React appellera deux fois votre réducteur et votre fonction d’initialisation afin de vous aider à détecter 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 (ce qui devrait être le cas), ç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 de dispatch, 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 avec Object.is, React sautera le nouveau rendu du composant et de ses enfants. C’est une optimisation. React aura peut-être quand même besoin d’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énements ont été 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 utiliser flushSync.


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 :

  1. L’état actuel de cette variable d’état, défini initialement avec l’état initial que vous avez fourni.
  2. 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 depuis les gestionnaires d’événements vers une seule fonction à l’extérieur de votre composant. Apprenez-en davantage sur comment 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 locaux à votre composant. Chaque action décrit une seule interaction, même si ça amène à modifier plusieurs fois la donnée. L’état peut avoir n’importe quelle forme, 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.

Piège

L’état est en lecture seule. Ne modifiez pas les objets ou les tableaux dans l’état :

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Ne modifiez pas l’objet dans l’état de cette façon :
state.age = state.age + 1;
return state;
}

Au lieu de ça, renvoyez toujours de nouveaux objets depuis votre réducteur :

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Renvoyez plutôt un nouvel objet
return {
...state,
age: state.age + 1
};
}

Lisez nos pages Mettre à jour les objets d’un état et Mettre à jour les tableaux d’un état pour en apprendre davantage.

Exemples simples de useReducer

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 coûteux.

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, à savoir la fonction elle-même, 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.

La différence entre passer une fonction d'initialisation et un état initial directement

Exemple 1 sur 2 ·
Passer la fonction d’initialisation

Cet exemple passe la fonction d’initialisation, la fonction createInitialState ne s’exécute donc 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 la console m’affiche 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 planifie un nouveau rendu avec sa nouvelle valeur, mais n’affecte pas la variable JavaScript state dans votre gestionnaire d’événement 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 en place :

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’elle 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, levez 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”

(« Trop de rendus successifs », NdT)

Vous pouvez rencontrer l’erreur indiquant Too many re-renders. React limits the number of renders to prevent an infinite loop. (« Trop de rendus successifs. 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. Ça vient le plus souvent d’une erreur dans la spécification d’un gestionnaire d’événement :

// 🚩 Incorrect : appelle le gestionnaire pendant le rendu
return <button onClick={handleClick()}>Cliquez ici</button>

// ✅ Correct : passe le gestionnaire d’événement
return <button onClick={handleClick}>Cliquez ici</button>

// ✅ Correct : passe une fonction en ligne
return <button onClick={(e) => handleClick(e)}>Cliquez ici</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 d’appels 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

En 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 directement 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 que de le modifier en place :

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correct : remplacement par 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 à détecter des erreurs en l’appelant deux fois. Seuls les composants, les fonctions d’initialisation 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.