Cours JavaScript et React · Module 4: Context API et useReducer

useReducer - gestion avancée de l'état

7 min de lecture
Dans cette leçon8

Dans le centre de commandement d'un vaisseau spatial, un simple panneau avec un seul bouton ne suffit pas. Lorsque les systèmes de bord deviennent de plus en plus complexes - boucliers, moteurs, systèmes de navigation, alarmes - nous avons besoin d'une manière plus organisée de gérer l'état. Le hook useReducer est comme l'ordinateur central de bord qui reçoit des ordres (actions) et met à jour l'état de tous les systèmes selon des règles strictement définies.

Qu'est-ce que useReducer ?

useReducer est une alternative à useState, conçue pour gérer une logique d'état plus complexe. Au lieu de définir directement une nouvelle valeur, tu envoies une action (commande), et une fonction spéciale appelée reducer décide de la façon dont l'état doit changer.

Syntaxe de base

1const [state, dispatch] = useReducer(reducer, initialState);

Où :

  • state - l'état actuel (données des systèmes du vaisseau)
  • dispatch - la fonction pour envoyer des actions (donner des ordres)
  • reducer - une fonction qui retourne un nouvel état en fonction de l'état actuel et de l'action
  • initialState - la valeur de l'état initial

Analogie spatiale : Imagine dispatch comme le panneau de contrôle où tu appuies sur des boutons (actions). L'ordinateur de bord (reducer) reçoit la commande et calcule le nouvel état en fonction de l'état actuel du vaisseau. Tu ne modifies jamais les systèmes manuellement - toujours via l'ordinateur !

La fonction reducer

Un reducer est une fonction pure qui prend deux arguments : l'état actuel et une action. À partir de ceux-ci, elle retourne un nouvel objet d'état. On utilise le plus souvent une instruction switch pour gérer les différents types d'actions :

1function spaceshipReducer(state, action) {
2  switch (action.type) {
3    case 'ACTIVATE_SHIELDS':
4      return { ...state, shields: 100, status: 'defended' };
5
6    case 'FIRE_ENGINES':
7      return { ...state, speed: state.speed + action.payload, fuel: state.fuel - 10 };
8
9    case 'EMERGENCY_STOP':
10      return { ...state, speed: 0, status: 'stopped' };
11
12    default:
13      return state;
14  }
15}

Règles clés du reducer :

  • Retourne toujours un nouvel objet d'état (ne mute pas le précédent)
  • Gère le default - retourne l'état inchangé pour les actions inconnues
  • Le reducer doit être une fonction pure - pas d'effets de bord, pas d'aléatoire

Dispatch et objets action

Une action est un simple objet JavaScript avec un champ type décrivant le genre d'opération. Tu peux éventuellement ajouter un champ payload avec des données supplémentaires :

1// Action simple - type uniquement
2dispatch({ type: 'ACTIVATE_SHIELDS' });
3
4// Action avec des données (payload)
5dispatch({ type: 'FIRE_ENGINES', payload: 50 });
6
7// Action avec un payload complexe
8dispatch({
9  type: 'ADD_CREW_MEMBER',
10  payload: { name: 'Capitaine Nova', role: 'pilot' }
11});

Exemple pratique - panneau de contrôle du vaisseau

Voyons comment useReducer se comporte dans la gestion de l'état complexe d'un panneau de contrôle :

1import { useReducer } from 'react';
2
3const initialShipState = {
4  fuel: 100,
5  shields: 0,
6  speed: 0,
7  status: 'docked',
8  log: []
9};
10
11function shipReducer(state, action) {
12  switch (action.type) {
13    case 'LAUNCH':
14      if (state.fuel < 20) return state;
15      return {
16        ...state,
17        status: 'flying',
18        fuel: state.fuel - 20,
19        speed: 50,
20        log: [...state.log, 'Vaisseau lancé']
21      };
22
23    case 'BOOST':
24      if (state.status !== 'flying' || state.fuel < 10) return state;
25      return {
26        ...state,
27        speed: state.speed + action.payload,
28        fuel: state.fuel - 10,
29        log: [...state.log, `Accéléré de ${action.payload} km/s`]
30      };
31
32    case 'TOGGLE_SHIELDS':
33      return {
34        ...state,
35        shields: state.shields > 0 ? 0 : 100,
36        log: [...state.log, state.shields > 0 ? 'Boucliers désactivés' : 'Boucliers activés']
37      };
38
39    case 'DOCK':
40      return {
41        ...state,
42        status: 'docked',
43        speed: 0,
44        log: [...state.log, 'Vaisseau amarré']
45      };
46
47    case 'REFUEL':
48      return {
49        ...state,
50        fuel: 100,
51        log: [...state.log, 'Carburant rempli']
52      };
53
54    default:
55      return state;
56  }
57}
58
59function ShipControlPanel() {
60  const [ship, dispatch] = useReducer(shipReducer, initialShipState);
61
62  return (
63    <div className="control-panel">
64      <h2>Panneau de contrôle</h2>
65      <p>Statut : {ship.status}</p>
66      <p>Carburant : {ship.fuel}%</p>
67      <p>Boucliers : {ship.shields}%</p>
68      <p>Vitesse : {ship.speed} km/s</p>
69
70      <button onClick={() => dispatch({ type: 'LAUNCH' })}
71        disabled={ship.status === 'flying'}>
72        Lancer
73      </button>
74      <button onClick={() => dispatch({ type: 'BOOST', payload: 25 })}
75        disabled={ship.status !== 'flying'}>
76        Accélérer (+25)
77      </button>
78      <button onClick={() => dispatch({ type: 'TOGGLE_SHIELDS' })}>
79        {ship.shields > 0 ? 'Désactiver les boucliers' : 'Activer les boucliers'}
80      </button>
81      <button onClick={() => dispatch({ type: 'DOCK' })}
82        disabled={ship.status === 'docked'}>
83        Amarrer
84      </button>
85      <button onClick={() => dispatch({ type: 'REFUEL' })}
86        disabled={ship.status === 'flying'}>
87        Faire le plein
88      </button>
89
90      <div className="ship-log">
91        <h3>Journal de bord :</h3>
92        <ul>
93          {ship.log.map((entry, i) => (
94            <li key={i}>{entry}</li>
95          ))}
96        </ul>
97      </div>
98    </div>
99  );
100}

Remarque comment le reducer centralise toute la logique de mise à jour de l'état. Le composant n'a pas besoin de savoir exactement comment l'état change - il lui suffit d'envoyer l'action appropriée. C'est un énorme avantage pour les interfaces complexes.

Quand utiliser useReducer plutôt que useState ?

Les deux hooks servent à gérer l'état, mais ils ont des cas d'usage différents :

Choisis useState quand :

  • L'état est simple (nombre, chaîne, booléen)
  • La logique de mise à jour est simple (affectation directe)
  • Tu as plusieurs valeurs indépendantes

Choisis useReducer quand :

  • L'état est un objet complexe avec de nombreux champs
  • Mettre à jour un champ nécessite de modifier d'autres champs
  • Tu as de nombreuses actions liées (par ex. démarrer, arrêter, réinitialiser, accélérer)
  • La logique de mise à jour contient des conditions (par ex. ne pas lancer sans carburant)
  • Tu veux pouvoir tester facilement la logique d'état séparément du composant
1// useState - suffisant pour les cas simples
2const [isOpen, setIsOpen] = useState(false);
3const [count, setCount] = useState(0);
4
5// useReducer - meilleur pour un état complexe
6const [shipState, dispatch] = useReducer(shipReducer, {
7  fuel: 100,
8  shields: 0,
9  speed: 0,
10  status: 'docked',
11  crew: [],
12  alerts: [],
13  systems: { engine: 'online', navigation: 'online', weapons: 'offline' }
14});

Le pattern switch-case dans les reducers

L'instruction switch est le pattern standard dans les reducers. Voici quelques bonnes pratiques :

1// Définis des constantes pour les types d'actions - évite les fautes de frappe
2const ACTIONS = {
3  LAUNCH: 'LAUNCH',
4  DOCK: 'DOCK',
5  BOOST: 'BOOST',
6  TAKE_DAMAGE: 'TAKE_DAMAGE',
7  REPAIR: 'REPAIR'
8};
9
10function shipReducer(state, action) {
11  switch (action.type) {
12    case ACTIONS.LAUNCH:
13      return { ...state, status: 'flying', speed: 50 };
14
15    case ACTIONS.DOCK:
16      return { ...state, status: 'docked', speed: 0 };
17
18    case ACTIONS.BOOST:
19      return {
20        ...state,
21        speed: Math.min(state.speed + action.payload, 300)
22      };
23
24    case ACTIONS.TAKE_DAMAGE: {
25      const newShields = Math.max(0, state.shields - action.payload);
26      return {
27        ...state,
28        shields: newShields,
29        status: newShields === 0 ? 'critical' : state.status
30      };
31    }
32
33    case ACTIONS.REPAIR:
34      return { ...state, shields: 100, status: 'docked' };
35
36    default:
37      throw new Error(`Action inconnue : ${action.type}`);
38  }
39}
40
41// Utilisation avec les constantes
42dispatch({ type: ACTIONS.BOOST, payload: 50 });
43dispatch({ type: ACTIONS.TAKE_DAMAGE, payload: 30 });

Remarque que dans TAKE_DAMAGE nous avons utilisé un bloc {} à l'intérieur du case - cela permet de déclarer des variables locales (newShields) sans conflits avec les autres cas. De plus, au lieu de default: return state, nous lançons une erreur - cela aide à repérer les fautes de frappe dans les noms d'actions pendant le développement.

Action creators

Pour plus de commodité et de lisibilité, tu peux créer des fonctions qui génèrent des objets action :

1// Action creators
2const launch = () => ({ type: 'LAUNCH' });
3const boost = (amount) => ({ type: 'BOOST', payload: amount });
4const takeDamage = (amount) => ({ type: 'TAKE_DAMAGE', payload: amount });
5const addCrewMember = (name, role) => ({
6  type: 'ADD_CREW_MEMBER',
7  payload: { name, role }
8});
9
10// Utilisation - plus lisible que de créer manuellement des objets
11dispatch(launch());
12dispatch(boost(50));
13dispatch(takeDamage(25));
14dispatch(addCrewMember('Nova', 'pilot'));

Les action creators réduisent le risque de fautes de frappe et centralisent la logique de création des actions en un seul endroit.

Résumé

useReducer est un outil puissant pour gérer un état complexe dans React :

  1. Syntaxe : const [state, dispatch] = useReducer(reducer, initialState)
  2. Reducer : une fonction pure (state, action) => newState avec le pattern switch-case
  3. Dispatch : envoyer des actions dispatch({ type: 'ACTION', payload: data })
  4. Quand l'utiliser : état complexe, mises à jour liées, logique métier dans l'état
  5. Bonnes pratiques : constantes pour les types d'actions, action creators, blocs {} dans les case, lancer des erreurs dans le default

Tout comme l'ordinateur central de bord d'un vaisseau spatial coordonne le travail de tous les systèmes à partir des commandes de la passerelle de commandement, useReducer centralise la logique d'état, la rendant prévisible et facile à gérer.

Code de cette leçon : App.jsx
1import React, { useState, useMemo, useCallback, memo } from 'react';
2
3// Composant sans optimisation (pour comparaison)
4function UnoptimizedItem({ name, onSelect }) {
5  console.log('Rendu de UnoptimizedItem :', name);
6  return (
7    <div style={{
8      padding: '10px',
9      backgroundColor: '#ffebee',
10      marginBottom: '5px',
11      borderRadius: '4px'
12    }}>
13      {name}
14      <button onClick={() => onSelect(name)} style={{ marginLeft: '10px' }}>Sélectionner</button>
15    </div>
16  );
17}
18
19// Composant optimisé avec React.memo
20const OptimizedItem = memo(function OptimizedItem({ name, onSelect }) {
21  console.log('Rendu de OptimizedItem :', name);
22  return (
23    <div style={{
24      padding: '10px',
25      backgroundColor: '#e8f5e9',
26      marginBottom: '5px',
27      borderRadius: '4px'
28    }}>
29      {name}
30      <button onClick={() => onSelect(name)} style={{ marginLeft: '10px' }}>Sélectionner</button>
31    </div>
32  );
33});
34
35// Calculs lourds (simulation)
36function expensiveCalculation(numbers) {
37  console.log('Exécution d\'un calcul coûteux...');
38  let result = 0;
39  for (let i = 0; i < 100000000; i++) {
40    result += numbers.reduce((a, b) => a + b, 0);
41  }
42  return numbers.reduce((a, b) => a + b, 0);
43}
44
45function PerformanceDemo() {
46  const [count, setCount] = useState(0);
47  const [numbers] = useState([1, 2, 3, 4, 5]);
48  const [selectedItem, setSelectedItem] = useState('');
49  const [darkMode, setDarkMode] = useState(false);
50
51  const items = ['Mars', 'Jupiter', 'Saturne', 'Uranus', 'Neptune'];
52
53  // Sans useMemo - calcul à chaque rendu
54  // const sum = expensiveCalculation(numbers);
55
56  // Avec useMemo - calcul uniquement quand 'numbers' change
57  const sum = useMemo(() => {
58    console.log('Calcul de la somme avec useMemo...');
59    return numbers.reduce((a, b) => a + b, 0);
60  }, [numbers]);
61
62  // Sans useCallback - nouvelle fonction à chaque rendu
63  // const handleSelect = (name) => setSelectedItem(name);
64
65  // Avec useCallback - référence stable
66  const handleSelect = useCallback((name) => {
67    setSelectedItem(name);
68  }, []);
69
70  // Styles optimisés avec useMemo
71  const containerStyle = useMemo(() => ({
72    fontFamily: 'Arial, sans-serif',
73    padding: '20px',
74    maxWidth: '900px',
75    margin: '0 auto',
76    backgroundColor: darkMode ? '#1a1a2e' : '#f5f5f5',
77    color: darkMode ? 'white' : '#333',
78    minHeight: '100vh',
79    transition: 'all 0.3s ease'
80  }), [darkMode]);
81
82  return (
83    <div style={containerStyle}>
84      <h1 style={{ textAlign: 'center', color: '#667eea' }}>Optimisation des performances</h1>
85
86      <div style={{
87        display: 'flex',
88        justifyContent: 'space-between',
89        alignItems: 'center',
90        marginBottom: '30px',
91        padding: '15px',
92        backgroundColor: darkMode ? '#252545' : '#fff',
93        borderRadius: '12px'
94      }}>
95        <div>
96          <button
97            onClick={() => setCount(c => c + 1)}
98            style={{
99              padding: '10px 20px',
100              backgroundColor: '#667eea',
101              color: 'white',
102              border: 'none',
103              borderRadius: '8px',
104              cursor: 'pointer',
105              marginRight: '10px'
106            }}
107          >
108            Compteur : {count}
109          </button>
110          <button
111            onClick={() => setDarkMode(!darkMode)}
112            style={{
113              padding: '10px 20px',
114              backgroundColor: darkMode ? '#fff' : '#333',
115              color: darkMode ? '#333' : 'white',
116              border: 'none',
117              borderRadius: '8px',
118              cursor: 'pointer'
119            }}
120          >
121            {darkMode ? 'Clair' : 'Sombre'}
122          </button>
123        </div>
124        <div>
125          Sélectionné : <strong>{selectedItem || 'Aucun'}</strong>
126        </div>
127      </div>
128
129      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
130        {/* Démo useMemo */}
131        <section style={{
132          padding: '20px',
133          backgroundColor: darkMode ? '#252545' : '#fff',
134          borderRadius: '12px'
135        }}>
136          <h2 style={{ color: '#4caf50' }}>useMemo</h2>
137          <p>Somme du tableau : <strong>{sum}</strong></p>
138          <p style={{ fontSize: '14px', color: '#888' }}>
139            Calculs effectués uniquement quand le tableau 'numbers' change.
140            Clique sur « Compteur » - la somme ne sera pas recalculée !
141          </p>
142          <pre style={{
143            backgroundColor: darkMode ? '#1a1a2e' : '#f5f5f5',
144            padding: '10px',
145            borderRadius: '8px',
146            fontSize: '12px',
147            overflow: 'auto'
148          }}>
149{`const sum = useMemo(() => {
150  return numbers.reduce((a, b) => a + b, 0);
151}, [numbers]);`}
152          </pre>
153        </section>
154
155        {/* Démo React.memo */}
156        <section style={{
157          padding: '20px',
158          backgroundColor: darkMode ? '#252545' : '#fff',
159          borderRadius: '12px'
160        }}>
161          <h2 style={{ color: '#2196f3' }}>React.memo + useCallback</h2>
162          <p style={{ fontSize: '14px', color: '#888', marginBottom: '15px' }}>
163            Regarde la console ! Les éléments optimisés ne se re-rendent pas quand le compteur change.
164          </p>
165
166          <h4>Sans optimisation :</h4>
167          {items.slice(0, 2).map(item => (
168            <UnoptimizedItem
169              key={item}
170              name={item}
171              onSelect={(name) => setSelectedItem(name)}
172            />
173          ))}
174
175          <h4 style={{ marginTop: '20px' }}>Avec React.memo + useCallback :</h4>
176          {items.slice(0, 2).map(item => (
177            <OptimizedItem
178              key={item}
179              name={item}
180              onSelect={handleSelect}
181            />
182          ))}
183        </section>
184      </div>
185
186      <section style={{
187        marginTop: '20px',
188        padding: '20px',
189        backgroundColor: darkMode ? '#252545' : '#fff',
190        borderRadius: '12px'
191      }}>
192        <h2 style={{ color: '#ff9800' }}>Quand optimiser ?</h2>
193        <ul style={{ lineHeight: '1.8' }}>
194          <li><strong>useMemo :</strong> Calculs coûteux, création d'objets/tableaux</li>
195          <li><strong>useCallback :</strong> Callbacks transmis à des enfants optimisés</li>
196          <li><strong>React.memo :</strong> Composants qui changent rarement</li>
197          <li><strong>Note :</strong> N'optimise pas prématurément ! Mesure d'abord.</li>
198        </ul>
199      </section>
200    </div>
201  );
202}
203
204export default PerformanceDemo;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que retourne le hook useReducer ?

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Construis une déclaration correcte de useReducer :

  • Éditeur de code

    Crée un système de station de recherche

  • Tri horizontal

    Ordonne la structure correcte d'un objet action dans useReducer :

  • Tri vertical

    Ordonne les composants d'une architecture d'application spatiale du plus général au plus spécifique :

  • Éditeur de code

    Crée une banque de données galactique

Articles utiles