Cours JavaScript et React · Module 3: État et événements

Mettre à jour l'état - patterns et pièges

8 min de lecture
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 ?

  • useState sé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 :

  1. Mise à jour fonctionnelle (prev => prev + 1) - à utiliser quand la nouvelle valeur dépend de la précédente
  2. Ne jamais muter l'état - crée toujours de nouveaux tableaux/objets (opérateur spread)
  3. Batching - React regroupe les mises à jour et fait un seul rendu du composant
  4. Stale closures - dans les callbacks et les timers, utilise les mises à jour fonctionnelles
  5. Plusieurs useState vs objet - dépend de la relation logique des données
  6. 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. 1. Pourquoi faut-il utiliser les mises à jour fonctionnelles (prev => prev + 1) quand on appelle setState plusieurs fois dans un même gestionnaire ?

  2. 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 :

Articles utiles