Cours JavaScript et React · Module 3: État et événements
Mettre à jour l'état - patterns et pièges
Dans cette leçon8
Imagine que les systèmes de navigation de ton vaisseau fonctionnent à partir de données d'état - vitesse, cap, niveau de carburant. Chaque mise à jour de ces paramètres doit être précise, car même une petite erreur peut faire sortir le vaisseau de son orbite. En React, les mises à jour de l'état suivent des règles similaires - tu dois connaître les patterns de mise à jour sûre pour que ton application se comporte de manière prévisible.
Mise à jour fonctionnelle vs valeur directe
Quand tu appelles setState, tu as deux options. La première consiste à passer directement une nouvelle valeur :
1setCount(count + 1);La seconde est une mise à jour fonctionnelle - passer une fonction qui reçoit la valeur précédente de l'état :
1setCount(prev => prev + 1);Pourquoi est-ce important ? React regroupe les mises à jour de l'état (batching) et ne les exécute pas immédiatement. Si tu appelles setCount(count + 1) plusieurs fois dans un même gestionnaire, chaque appel voit la même valeur de count :
1function FuelGauge() {
2 const [fuel, setFuel] = useState(50);
3
4 const handleTripleBoost = () => {
5 // Les trois voient la même valeur de fuel !
6 // Résultat : fuel n'augmentera que de 10, pas de 30
7 setFuel(fuel + 10);
8 setFuel(fuel + 10);
9 setFuel(fuel + 10);
10 };
11
12 const handleTripleBoostCorrect = () => {
13 // Chaque appel reçoit la valeur actuelle
14 // Résultat : fuel augmentera de 30
15 setFuel(prev => prev + 10);
16 setFuel(prev => prev + 10);
17 setFuel(prev => prev + 10);
18 };
19
20 return (
21 <div>
22 <p>Carburant : {fuel}%</p>
23 <button onClick={handleTripleBoost}>Boost (erroné)</button>
24 <button onClick={handleTripleBoostCorrect}>Boost (correct)</button>
25 </div>
26 );
27}Règle : Utilise toujours des mises à jour fonctionnelles (prev => ...) quand la nouvelle valeur dépend de la précédente.
Plusieurs variables d'état vs un seul objet
Tu as deux stratégies pour stocker l'état dans un composant. La première : des variables d'état séparées :
1function ShipDashboard() {
2 const [speed, setSpeed] = useState(0);
3 const [altitude, setAltitude] = useState(0);
4 const [heading, setHeading] = useState(0);
5
6 // Chaque variable mise à jour indépendamment
7 const accelerate = () => setSpeed(prev => prev + 10);
8 const climb = () => setAltitude(prev => prev + 100);
9
10 return (
11 <div>
12 <p>Vitesse : {speed} km/s | Altitude : {altitude} km | Cap : {heading}°</p>
13 <button onClick={accelerate}>Accélérer</button>
14 <button onClick={climb}>Monter</button>
15 </div>
16 );
17}La seconde : un état dans un objet - utile quand les données sont liées :
1function ShipDashboard() {
2 const [navigation, setNavigation] = useState({
3 speed: 0,
4 altitude: 0,
5 heading: 0
6 });
7
8 // IMPORTANT : toujours copier l'objet entier avec l'opérateur spread !
9 const accelerate = () => {
10 setNavigation(prev => ({ ...prev, speed: prev.speed + 10 }));
11 };
12
13 return (
14 <div>
15 <p>Vitesse : {navigation.speed} km/s</p>
16 <button onClick={accelerate}>Accélérer</button>
17 </div>
18 );
19}Quand utiliser quoi ?
useStateséparés - quand les valeurs changent indépendamment les unes des autres- Objet dans
useState- quand les valeurs vont logiquement ensemble (par ex. un formulaire, une position x/y)
Les erreurs les plus courantes - la mutation de l'état
En React, l'état est immuable. Ne modifie jamais l'état directement - crée toujours une nouvelle copie. C'est comme une règle de sécurité sur un vaisseau spatial : tu n'édites pas le journal de bord, tu ajoutes simplement une nouvelle entrée.
Erreur 1 : push au lieu de spread pour les tableaux
1function MissionLog() {
2 const [logs, setLogs] = useState(["Début de la mission"]);
3
4 const addLog = (entry) => {
5 // ERREUR : push mute le tableau existant !
6 // React ne détectera pas le changement et ne redéclenchera pas le rendu du composant
7 logs.push(entry);
8 setLogs(logs);
9 };
10
11 const addLogCorrect = (entry) => {
12 // Spread crée un nouveau tableau
13 setLogs(prev => [...prev, entry]);
14 };
15
16 return (
17 <div>
18 <button onClick={() => addLogCorrect("Nouvelle entrée")}>
19 Ajouter une entrée
20 </button>
21 {logs.map((log, i) => <p key={i}>{log}</p>)}
22 </div>
23 );
24}Erreur 2 : modifier directement les propriétés d'un objet
1function CrewMember() {
2 const [member, setMember] = useState({
3 name: "Capitaine Nova",
4 rank: "Capitaine",
5 missions: 12
6 });
7
8 const promote = () => {
9 // ERREUR : mutation directe de l'objet
10 member.rank = "Amiral";
11 setMember(member);
12 };
13
14 const promoteCorrect = () => {
15 // Création d'un nouvel objet avec l'opérateur spread
16 setMember(prev => ({ ...prev, rank: "Amiral" }));
17 };
18
19 return (
20 <div>
21 <p>{member.name} - {member.rank}</p>
22 <button onClick={promoteCorrect}>Promouvoir</button>
23 </div>
24 );
25}Les stale closures - le piège des closures
Une closure est une situation où une fonction « se souvient » des variables au moment où elle a été créée. En React, cela peut conduire à lire un état périmé :
1function CountdownTimer() {
2 const [seconds, setSeconds] = useState(10);
3 const [running, setRunning] = useState(false);
4
5 const startCountdown = () => {
6 // seconds est "figé" au moment du clic !
7 // Chaque intervalle voit la même valeur
8 setInterval(() => {
9 setSeconds(seconds - 1); // toujours 10 - 1 = 9
10 }, 1000);
11 };
12
13 const startCountdownCorrect = () => {
14 setRunning(true);
15 // La mise à jour fonctionnelle utilise toujours la valeur actuelle
16 setInterval(() => {
17 setSeconds(prev => Math.max(prev - 1, 0));
18 }, 1000);
19 };
20
21 return (
22 <div>
23 <p>Compte à rebours : {seconds}s</p>
24 <button onClick={startCountdownCorrect} disabled={running}>Démarrer</button>
25 </div>
26 );
27}Le bouton avec disabled={running} ne te laisse pas lancer un deuxième intervalle, et Math.max(prev - 1, 0) arrête le compteur à zéro. L'intervalle lui-même continue pourtant de tourner tant que tu ne l'arrêtes pas avec clearInterval - tu découvriras le nettoyage propre des timers dans le module sur les effets (useEffect).
Le batching - regrouper les mises à jour
React 18+ regroupe automatiquement plusieurs mises à jour d'état dans un même gestionnaire d'événement, n'effectuant qu'un seul rendu. C'est comme l'ordinateur de bord qui collecte tous les rapports des capteurs et met à jour l'affichage une seule fois, au lieu de le rafraîchir après chaque capteur :
1function NavigationUpdate() {
2 const [position, setPosition] = useState({ x: 0, y: 0 });
3 const [velocity, setVelocity] = useState(0);
4 const [status, setStatus] = useState("idle");
5
6 const handleJump = () => {
7 // React regroupe ces 3 mises à jour en un seul rendu
8 setPosition({ x: 100, y: 200 });
9 setVelocity(9999);
10 setStatus("hyperjump");
11 // Le composant fait UN seul rendu, pas trois !
12 };
13
14 return (
15 <div>
16 <p>Position : ({position.x}, {position.y})</p>
17 <p>Vitesse : {velocity} | Statut : {status}</p>
18 <button onClick={handleJump}>Saut hyperspatial</button>
19 </div>
20 );
21}Patterns pratiques de mise à jour de l'état
Voici quelques patterns qui te seront utiles à bord du vaisseau React :
1// Supprimer un élément d'un tableau
2setItems(prev => prev.filter(item => item.id !== idToRemove));
3
4// Mettre à jour un élément d'un tableau
5setItems(prev => prev.map(item =>
6 item.id === targetId ? { ...item, status: "done" } : item
7));
8
9// Toggle (basculer un booléen)
10setIsActive(prev => !prev);
11
12// Ajouter un élément au début d'un tableau
13setMessages(prev => [newMessage, ...prev]);
14
15// Incrémenter avec une limite
16setFuel(prev => Math.min(prev + 10, 100));L'initialisation paresseuse de l'état
La valeur initiale n'est utile qu'au premier rendu. Pourtant, si tu appelles une fonction entre les parenthèses de useState, elle s'exécute à chaque rendu, même si React n'utilise son résultat que la première fois :
1// INCORRECT - computeStarMap() est recalculé à chaque rendu
2const [stars, setStars] = useState(computeStarMap());Pour un calcul coûteux (par ex. construire une carte des étoiles à partir de milliers de points), passe la fonction elle-même - React ne l'appelle qu'une fois, au premier rendu :
1// CORRECT - la fonction d'initialisation ne s'exécute qu'au premier rendu
2const [stars, setStars] = useState(() => computeStarMap());Tu passes la fonction (() => computeStarMap() ou simplement computeStarMap), et non son résultat - React mémorise la valeur et ne la recalcule plus lors des rendus suivants.
Résumé
Mettre à jour l'état en React - principes clés :
- Mise à jour fonctionnelle (
prev => prev + 1) - à utiliser quand la nouvelle valeur dépend de la précédente - Ne jamais muter l'état - crée toujours de nouveaux tableaux/objets (opérateur spread)
- Batching - React regroupe les mises à jour et fait un seul rendu du composant
- Stale closures - dans les callbacks et les timers, utilise les mises à jour fonctionnelles
- Plusieurs useState vs objet - dépend de la relation logique des données
- Initialisation paresseuse -
useState(() => calcul())calcule la valeur initiale une seule fois
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2
3function FuelManager() {
4 // Variables d'état séparées
5 const [fuel, setFuel] = useState(50);
6 const [oxygen, setOxygen] = useState(100);
7
8 // État sous forme d'objet
9 const [position, setPosition] = useState({ x: 0, y: 0, z: 0 });
10
11 // État sous forme de tableau
12 const [logs, setLogs] = useState([
13 { id: 1, message: 'Système démarré', time: '00:00' }
14 ]);
15
16 const [newLog, setNewLog] = useState('');
17
18 // Mise à jour fonctionnelle (prev =>)
19 const boostFuel = () => {
20 setFuel(prev => Math.min(prev + 10, 100));
21 };
22
23 const consumeFuel = () => {
24 setFuel(prev => Math.max(prev - 15, 0));
25 };
26
27 // Mise à jour d'objet avec spread
28 const moveShip = (axis, delta) => {
29 setPosition(prev => ({ ...prev, [axis]: prev[axis] + delta }));
30 };
31
32 // Ajout au tableau (spread, pas push !)
33 const addLog = () => {
34 if (!newLog.trim()) return;
35 const now = new Date().toLocaleTimeString().slice(0, 5);
36 setLogs(prev => [...prev, {
37 id: Date.now(),
38 message: newLog,
39 time: now
40 }]);
41 setNewLog('');
42 };
43
44 // Suppression du tableau (filter)
45 const removeLog = (id) => {
46 setLogs(prev => prev.filter(log => log.id !== id));
47 };
48
49 // Triple boost - comparaison de l'approche erronée et de l'approche correcte
50 const brokenTripleBoost = () => {
51 // fuel est "figé" - les 3 voient la même valeur
52 setFuel(fuel + 5);
53 setFuel(fuel + 5);
54 setFuel(fuel + 5);
55 };
56
57 const correctTripleBoost = () => {
58 // Chaque appel voit la valeur actuelle
59 setFuel(prev => Math.min(prev + 5, 100));
60 setFuel(prev => Math.min(prev + 5, 100));
61 setFuel(prev => Math.min(prev + 5, 100));
62 };
63
64 return (
65 <div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '700px', margin: '0 auto' }}>
66 <h1 style={{ textAlign: 'center', color: '#667eea' }}>Patterns de mise à jour de l'état</h1>
67
68 {/* Section carburant */}
69 <div style={{ padding: '16px', marginBottom: '16px', backgroundColor: '#f0f4ff', borderRadius: '12px' }}>
70 <h3>Carburant : {fuel}%</h3>
71 <div style={{ height: '20px', backgroundColor: '#ddd', borderRadius: '10px', overflow: 'hidden' }}>
72 <div style={{
73 height: '100%', width: fuel + '%',
74 backgroundColor: fuel > 30 ? '#4caf50' : '#f44336',
75 transition: 'width 0.3s'
76 }} />
77 </div>
78 <div style={{ marginTop: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
79 <button onClick={boostFuel} style={btnStyle('#4caf50')}>+10 (fonctionnel)</button>
80 <button onClick={consumeFuel} style={btnStyle('#f44336')}>-15 (fonctionnel)</button>
81 <button onClick={brokenTripleBoost} style={btnStyle('#ff9800')}>3x +5 (erroné)</button>
82 <button onClick={correctTripleBoost} style={btnStyle('#2196f3')}>3x +5 (correct)</button>
83 </div>
84 </div>
85
86 {/* Section position (objet) */}
87 <div style={{ padding: '16px', marginBottom: '16px', backgroundColor: '#f0fff4', borderRadius: '12px' }}>
88 <h3>Position du vaisseau (objet avec spread)</h3>
89 <p>X: {position.x} | Y: {position.y} | Z: {position.z}</p>
90 <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
91 <button onClick={() => moveShip('x', 10)} style={btnStyle('#667eea')}>X +10</button>
92 <button onClick={() => moveShip('y', 10)} style={btnStyle('#667eea')}>Y +10</button>
93 <button onClick={() => moveShip('z', 10)} style={btnStyle('#667eea')}>Z +10</button>
94 <button onClick={() => setPosition({ x: 0, y: 0, z: 0 })} style={btnStyle('#999')}>Reset</button>
95 </div>
96 </div>
97
98 {/* Section journal (tableau) */}
99 <div style={{ padding: '16px', backgroundColor: '#fff8f0', borderRadius: '12px' }}>
100 <h3>Journal de bord (tableau avec spread)</h3>
101 <div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
102 <input
103 value={newLog}
104 onChange={e => setNewLog(e.target.value)}
105 placeholder="Nouvelle entrée..."
106 style={{ flex: 1, padding: '8px', borderRadius: '8px', border: '1px solid #ddd' }}
107 />
108 <button onClick={addLog} style={btnStyle('#ff9800')}>Ajouter</button>
109 </div>
110 {logs.map(log => (
111 <div key={log.id} style={{
112 display: 'flex', justifyContent: 'space-between', alignItems: 'center',
113 padding: '8px 12px', marginBottom: '6px', backgroundColor: '#fff',
114 borderRadius: '8px', border: '1px solid #eee'
115 }}>
116 <span><strong>[{log.time}]</strong> {log.message}</span>
117 <button onClick={() => removeLog(log.id)} style={{
118 background: 'none', border: 'none', color: '#f44336', cursor: 'pointer', fontSize: '16px'
119 }}>x</button>
120 </div>
121 ))}
122 </div>
123 </div>
124 );
125}
126
127function btnStyle(bg) {
128 return {
129 padding: '8px 16px', backgroundColor: bg, color: 'white',
130 border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '13px'
131 };
132}
133
134export default function App() {
135 return <FuelManager />;
136}Souviens-toi : L'état en React est comme le journal de bord du vaisseau - tu ne l'édites jamais, tu crées simplement une nouvelle entrée à partir de la précédente !
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. Pourquoi faut-il utiliser les mises à jour fonctionnelles (prev => prev + 1) quand on appelle setState plusieurs fois dans un même gestionnaire ?
2. Pourquoi le code suivant est-il incorrect ? const [items, setItems] = useState(['A', 'B']); items.push('C'); setItems(items);
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Remets dans l'ordre ce qui se passe lorsque React regroupe plusieurs mises à jour d'état :