Avancé30 min de lecture

useReducer

Apprends le pattern reducer pour gérer un état complexe dans React. Comprends quand utiliser useReducer plutôt que useState, comment dispatcher des actions, et comment combiner les reducers avec le contexte.

Quand useState ne suffit pas

useState est parfait pour des morceaux d'état simples et indépendants : un compteur, un toggle, un champ de formulaire. Mais à mesure que ton composant grandit, tu peux te retrouver à jongler avec plusieurs variables d'état liées qui changent de manière coordonnée.

Considère un composant de liste de tâches :

jsx
function TodoApp() {
  const [todos, setTodos] = React.useState([]);
  const [filter, setFilter] = React.useState('all');
  const [editingId, setEditingId] = React.useState(null);

  function addTodo(text) {
    setTodos([...todos, { id: Date.now(), text, done: false }]);
  }

  function toggleTodo(id) {
    setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }

  function deleteTodo(id) {
    setTodos(todos.filter(t => t.id !== id));
    if (editingId === id) setEditingId(null); // mise à jour coordonnée !
  }
  // ... plus de handlers
}

Remarque comment deleteTodo doit mettre à jour deux morceaux d'état, et comment chaque handler contient la logique pour calculer l'état suivant. À mesure que le composant grandit, ces fonctions de mise à jour deviennent complexes et sujettes aux erreurs.

C'est là que useReducer brille. Il te permet de :

  • Centraliser toute la logique de mise à jour de l'état en un seul endroit (la fonction reducer)
  • Décrire les mises à jour comme des actions ("ce qui s'est passé") plutôt que des mutations d'état impératives
  • Garder les transitions d'état prévisibles et faciles à tester
  • Séparer le "ce qui s'est passé" (dispatcher une action) du "comment mettre à jour" (le reducer)

Le pattern Reducer

Un reducer est une fonction pure qui prend l'état actuel et une action, et retourne le nouvel état :

(state, action) => newState

Ce pattern vient de la méthode Array.reduce() et est la fondation de bibliothèques comme Redux. L'idée clé est que toutes les transitions d'état sont décrites par des actions — des objets simples qui disent ce qui s'est passé.

Objets action

Les actions sont de simples objets JavaScript avec une propriété type (une chaîne de caractères qui décrit l'événement) et un payload optionnel (données supplémentaires) :

javascript
{ type: 'ADD_TODO', payload: { text: 'Learn React' } }
{ type: 'TOGGLE_TODO', payload: { id: 1 } }
{ type: 'DELETE_TODO', payload: { id: 1 } }
{ type: 'CLEAR_ALL' }  // pas de payload nécessaire

La fonction reducer

Le reducer examine le type d'action et calcule le nouvel état en conséquence :

javascript
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload.text,
          done: false,
        }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, done: !todo.done }
            : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id),
      };
    default:
      return state;
  }
}

Règles importantes pour les reducers :

  1. Fonction pure — les mêmes entrées produisent toujours les mêmes sorties. Pas d'effets de bord (pas d'appels API, pas de localStorage, pas de Math.random()).
  2. Ne jamais muter l'état — toujours retourner un nouvel objet. Utilise l'opérateur spread (...state) pour copier les propriétés existantes.
  3. Gérer les actions inconnues — le cas default doit retourner l'état actuel inchangé.

Syntaxe de useReducer et dispatch

Le hook useReducer de React prend une fonction reducer et un état initial, et retourne l'état actuel plus une fonction dispatch :

jsx
const [state, dispatch] = React.useReducer(reducer, initialState);
  • state — la valeur actuelle de l'état
  • dispatch — une fonction que tu appelles pour envoyer des actions au reducer

Dispatcher des actions

Pour déclencher une mise à jour de l'état, appelle dispatch avec un objet action :

jsx
function TodoApp() {
  const [state, dispatch] = React.useReducer(todoReducer, { todos: [] });

  function handleAdd(text) {
    dispatch({ type: 'ADD_TODO', payload: { text } });
  }

  function handleToggle(id) {
    dispatch({ type: 'TOGGLE_TODO', payload: { id } });
  }

  return (
    <div>
      <button onClick={() => handleAdd('New task')}>Add</button>
      <ul>
        {state.todos.map(todo => (
          <li
            key={todo.id}
            onClick={() => handleToggle(todo.id)}
            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

Quand tu appelles dispatch, React :

  1. Appelle ton reducer avec l'état actuel et l'action
  2. Prend la valeur de retour comme nouvel état
  3. Re-rend le composant avec le nouvel état

Quand choisir useReducer plutôt que useState

Utilise useState quand...Utilise useReducer quand...
L'état est une valeur unique (nombre, chaîne de caractères, booléen)L'état est un objet ou un tableau avec plusieurs sous-valeurs
Les mises à jour sont simples (définir une nouvelle valeur)Les mises à jour dépendent de l'état précédent de manières complexes
Peu de transitions d'étatBeaucoup de transitions d'état différentes
La logique est simpleTu veux centraliser et tester la logique de mise à jour

Combiner useReducer avec Context

L'un des patterns les plus puissants dans React est de combiner useReducer avec Context. Cela te donne une solution de gestion d'état simple et intégrée qui peut remplacer Redux pour de nombreux cas d'usage.

L'idée : stocker le state et le dispatch de ton reducer dans le contexte pour que n'importe quel composant dans l'arbre puisse lire l'état ou dispatcher des actions sans prop drilling.

jsx
const TodoContext = React.createContext();

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload.text,
          done: false,
        }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.payload.id ? { ...t, done: !t.done } : t
        ),
      };
    default:
      return state;
  }
}

function TodoProvider({ children }) {
  const [state, dispatch] = React.useReducer(todoReducer, { todos: [] });
  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
}

// N'importe quel composant profondément imbriqué peut dispatcher des actions :
function AddButton() {
  const { dispatch } = React.useContext(TodoContext);
  return (
    <button onClick={() => dispatch({
      type: 'ADD_TODO',
      payload: { text: 'New task' }
    })}>
      Add Todo
    </button>
  );
}

// N'importe quel composant profondément imbriqué peut lire l'état :
function TodoList() {
  const { state } = React.useContext(TodoContext);
  return (
    <ul>
      {state.todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Ce pattern est léger et ne nécessite aucune bibliothèque tierce. Il fonctionne bien pour les applications de taille moyenne. Pour les très grandes applications avec des mises à jour fréquentes, les bibliothèques de gestion d'état dédiées peuvent offrir de meilleures performances grâce aux souscriptions sélectives.

useReducer en action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  function counterReducer(state, action) {
    switch (action.type) {
      case "INCREMENT":
        return { ...state, count: state.count + 1 };
      case "DECREMENT":
        return { ...state, count: state.count - 1 };
      case "RESET":
        return { ...state, count: 0 };
      case "SET":
        return { ...state, count: action.payload };
      default:
        return state;
    }
  }

  function App() {
    const [state, dispatch] = React.useReducer(counterReducer, { count: 0 });

    return (
      <div>
        <h2>useReducer Counter</h2>
        <p>Count: {state.count}</p>
        <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button>
        <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button>
        <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
        <button onClick={() => dispatch({ type: "SET", payload: 100 })}>Set to 100</button>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

Quels sont les deux arguments qu'une fonction reducer reçoit ?

Prêt à pratiquer ?

Crée ton compte gratuit pour accéder à l'éditeur de code interactif, lancer les défis et suivre ta progression.