Cours JavaScript et React · Module 4: Context API et useReducer
useReducer - gestion avancée de l'état
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'actioninitialState- 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 :
- Syntaxe :
const [state, dispatch] = useReducer(reducer, initialState) - Reducer : une fonction pure
(state, action) => newStateavec le pattern switch-case - Dispatch : envoyer des actions
dispatch({ type: 'ACTION', payload: data }) - Quand l'utiliser : état complexe, mises à jour liées, logique métier dans l'état
- 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. 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