Cours JavaScript et React · Module 3: État et événements
L'état d'un composant (State)
Dans cette leçon6
Lors d'un voyage cosmique à travers la galaxie React, l'état est comme le carburant de la fusée qui alimente l'interactivité de tes composants. Sans état, les composants seraient des objets statiques - comme des planètes sans atmosphère. L'état permet aux composants de « se souvenir » d'informations et de réagir aux changements, rendant tes applications vivantes et réactives.
Qu'est-ce que l'état d'un composant ?
L'état est une donnée (un nombre, un texte, un booléen, un tableau ou un objet) dont le composant se souvient entre deux rendus. Contrairement aux props, que le parent transmet au composant, l'état appartient au composant lui-même et lui seul peut le modifier - toujours via sa fonction de mise à jour, par ex. setFuel, jamais en le modifiant directement.
Imagine le tableau de bord d'un vaisseau spatial. Les props sont comme des coordonnées de destination fixées - tu ne peux pas les changer depuis le tableau de bord. L'état, en revanche, est comme les relevés actuels de vitesse, d'altitude et de température - des données qui changent constamment et qui sont contrôlées par le tableau de bord lui-même.
Gérer l'état dans les composants fonctionnels
Dans le React moderne, l'état des composants fonctionnels est géré à l'aide du hook useState. Ce hook simple mais puissant te permet d'ajouter un état aux composants fonctionnels.
Syntaxe de base de useState
1import React, { useState } from 'react';
2
3function ControlPanel() {
4 // Déclaration d'une variable d'état "fuel" avec une valeur initiale de 100
5 const [fuel, setFuel] = useState(100);
6
7 // fuel - la valeur actuelle de l'état
8 // setFuel - la fonction pour mettre à jour l'état
9
10 return (
11 <div className="control-panel">
12 <h2>Panneau de commande</h2>
13 <p>Niveau de carburant : {fuel}%</p>
14 <button onClick={() => setFuel(fuel - 10)}>
15 Consommer du carburant
16 </button>
17 </div>
18 );
19}Dans l'exemple ci-dessus :
- On importe le hook
useStatedepuis React - On appelle
useState(100)pour créer une variable d'étatfuelavec une valeur initiale de 100 - On déstructure le résultat pour obtenir :
fuel: la valeur actuelle de l'étatsetFuel: la fonction pour mettre à jour cette valeur
- On utilise
setFueldans le gestionnaire de l'événement de clic pour diminuer la valeur defuelde 10
Utiliser plusieurs variables d'état
Un composant peut avoir plusieurs variables d'état indépendantes :
1function SpaceshipDashboard() {
2 const [fuel, setFuel] = useState(100);
3 const [speed, setSpeed] = useState(0);
4 const [shields, setShields] = useState(true);
5 const [destination, setDestination] = useState('Orbite');
6
7 return (
8 <div className="dashboard">
9 <h2>Tableau de bord du vaisseau</h2>
10 <div className="gauges">
11 <div className="gauge">
12 <h3>Carburant</h3>
13 <p>{fuel}%</p>
14 <button onClick={() => setFuel(Math.max(0, fuel - 5))}>
15 Augmenter la poussée
16 </button>
17 </div>
18
19 <div className="gauge">
20 <h3>Vitesse</h3>
21 <p>{speed} km/s</p>
22 <button onClick={() => setSpeed(speed + 1)}>
23 Accélérer
24 </button>
25 <button onClick={() => setSpeed(Math.max(0, speed - 1))}>
26 Ralentir
27 </button>
28 </div>
29
30 <div className="gauge">
31 <h3>Boucliers</h3>
32 <p>{shields ? 'Activés' : 'Désactivés'}</p>
33 <button onClick={() => setShields(!shields)}>
34 Basculer
35 </button>
36 </div>
37
38 <div className="gauge">
39 <h3>Destination</h3>
40 <p>{destination}</p>
41 <select
42 value={destination}
43 onChange={(e) => setDestination(e.target.value)}
44 >
45 <option value="Orbite">Orbite terrestre</option>
46 <option value="Lune">Lune</option>
47 <option value="Mars">Mars</option>
48 <option value="Ceinture d'astéroïdes">Ceinture d'astéroïdes</option>
49 </select>
50 </div>
51 </div>
52 </div>
53 );
54}Structures d'état complexes
L'état peut être une valeur simple (nombre, chaîne, booléen), mais il peut aussi être une structure plus complexe, comme un tableau ou un objet :
1function CrewManagement() {
2 const [crew, setCrew] = useState([
3 { id: 1, name: 'Anna Nowak', role: 'Capitaine', onDuty: true },
4 { id: 2, name: 'Jan Kowalski', role: 'Navigateur', onDuty: false },
5 { id: 3, name: 'Maria Wisniewska', role: 'Ingénieure', onDuty: true }
6 ]);
7
8 const toggleDuty = (crewId) => {
9 setCrew(crew.map(member =>
10 member.id === crewId
11 ? { ...member, onDuty: !member.onDuty }
12 : member
13 ));
14 };
15
16 return (
17 <div className="crew-management">
18 <h2>Gestion de l'équipage</h2>
19 <ul className="crew-list">
20 {crew.map(member => (
21 <li key={member.id} className={member.onDuty ? 'on-duty' : 'off-duty'}>
22 <strong>{member.name}</strong> - {member.role}
23 <button onClick={() => toggleDuty(member.id)}>
24 {member.onDuty ? 'Terminer le service' : 'Commencer le service'}
25 </button>
26 </li>
27 ))}
28 </ul>
29 </div>
30 );
31}Dans cet exemple, l'état crew est un tableau d'objets. La fonction toggleDuty utilise map pour créer un nouveau tableau avec l'état mis à jour d'un membre d'équipage donné.
L'état comme un instantané
En React, l'état fonctionne comme un instantané (snapshot). Quand React effectue le rendu d'un composant, il « capture » l'état à cet instant, et toutes les valeurs à l'intérieur de la fonction de rendu (y compris les gestionnaires d'événements) « verront » le même instantané de l'état.
Considère cet exemple :
1function RocketLauncher() {
2 const [countdown, setCountdown] = useState(10);
3
4 const decrementCountdown = () => {
5 // Cela NE fonctionnera PAS comme prévu si l'on veut
6 // effectuer plusieurs décrémentations dans un même cycle de rendu
7 setCountdown(countdown - 1); // Utilise la même valeur de l'instantané
8 setCountdown(countdown - 1); // Utilise encore la même valeur !
9 };
10
11 return (
12 <div className="rocket-launcher">
13 <h2>Compte à rebours avant le lancement : {countdown}</h2>
14 <button onClick={decrementCountdown}>
15 Réduire le compte à rebours
16 </button>
17 </div>
18 );
19}Dans l'exemple ci-dessus, malgré le double appel à setCountdown, la valeur ne diminuera que de 1, et non de 2. Cela se produit parce que les deux mises à jour « voient » la même valeur de countdown issue de l'instantané.
Mettre à jour l'état à partir de l'état précédent
Pour résoudre le problème de l'instantané, React te permet de passer une fonction à la fonction de mise à jour de l'état. Cette fonction reçoit l'état précédent en argument :
1function RocketLauncher() {
2 const [countdown, setCountdown] = useState(10);
3
4 const decrementCountdown = () => {
5 // Cela fonctionnera correctement, même pour plusieurs mises à jour
6 setCountdown(prevCount => prevCount - 1); // À partir de la valeur précédente
7 setCountdown(prevCount => prevCount - 1); // À partir du résultat de la mise à jour précédente
8 };
9
10 return (
11 <div className="rocket-launcher">
12 <h2>Compte à rebours avant le lancement : {countdown}</h2>
13 <button onClick={decrementCountdown}>
14 Réduire le compte à rebours
15 </button>
16 </div>
17 );
18}Maintenant, à chaque clic sur le bouton, le compte à rebours diminuera de 2.
Mettre à jour des objets dans l'état
Lorsqu'on met à jour des objets dans l'état, il est important de toujours créer un nouvel objet plutôt que de modifier l'objet existant. Cela respecte les principes d'immutabilité de React :
1function SpaceshipSystems() {
2 const [systems, setSystems] = useState({
3 engines: 'offline',
4 lifeSupportOutput: 100,
5 navigationStatus: 'calibrating',
6 shields: 85
7 });
8
9 const startEngines = () => {
10 // INCORRECT - modifier l'objet original
11 // systems.engines = 'online';
12 // setSystems(systems);
13
14 // CORRECT - créer un nouvel objet
15 setSystems({
16 ...systems, // copie toutes les propriétés existantes
17 engines: 'online' // ne remplace que celle que l'on veut changer
18 });
19 };
20
21 return (
22 <div className="spaceship-systems">
23 <h2>État des systèmes</h2>
24 <p>Moteurs : {systems.engines}</p>
25 <p>Système de survie : {systems.lifeSupportOutput}%</p>
26 <p>Navigation : {systems.navigationStatus}</p>
27 <p>Boucliers : {systems.shields}%</p>
28
29 <button onClick={startEngines}>
30 Démarrer les moteurs
31 </button>
32 </div>
33 );
34}Mettre à jour des tableaux dans l'état
Comme pour les objets, lorsqu'on met à jour des tableaux il faut aussi respecter les principes d'immutabilité :
1function MissionLog() {
2 const [logs, setLogs] = useState([
3 { id: 1, time: '08:00', message: 'Début de la mission' }
4 ]);
5
6 const addLog = () => {
7 const newLog = {
8 id: Math.max(0, ...logs.map(log => log.id)) + 1, // plus grand id + 1 : il ne se répète pas après une suppression
9 time: new Date().toLocaleTimeString(),
10 message: 'Nouvelle entrée du journal'
11 };
12
13 // INCORRECT - muter le tableau original
14 // logs.push(newLog);
15 // setLogs(logs);
16
17 // CORRECT - créer un nouveau tableau
18 setLogs([...logs, newLog]); // ajouter à la fin
19 // ou
20 // setLogs([newLog, ...logs]); // ajouter au début
21 };
22
23 const removeLog = (logId) => {
24 // Filtrage - créer un nouveau tableau sans l'élément supprimé
25 setLogs(logs.filter(log => log.id !== logId));
26 };
27
28 return (
29 <div className="mission-log">
30 <h2>Journal de mission</h2>
31 <button onClick={addLog}>Ajouter une entrée</button>
32
33 <ul className="log-entries">
34 {logs.map(log => (
35 <li key={log.id}>
36 <span className="time">{log.time}</span>
37 <span className="message">{log.message}</span>
38 <button onClick={() => removeLog(log.id)}>Supprimer</button>
39 </li>
40 ))}
41 </ul>
42 </div>
43 );
44}Tu retrouves ces mêmes opérations sur un tableau - ajouter, basculer et supprimer une entrée - dans la démo ci-dessous, avec un compteur (un nombre), un interrupteur (un booléen) et un champ de texte (du texte). Regarde l'encadré en bas : on appelle useState uniquement au niveau supérieur d'un composant, jamais dans une boucle, une condition ou une fonction imbriquée, car React distingue les états grâce à l'ordre des appels à chaque rendu.
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2
3// Compteur simple - les bases de useState
4function SimpleCounter() {
5 const [count, setCount] = useState(0);
6
7 return (
8 <div style={{
9 backgroundColor: '#252545',
10 padding: '20px',
11 borderRadius: '12px',
12 textAlign: 'center'
13 }}>
14 <h3 style={{ color: '#00d4ff', marginTop: 0 }}>Compteur simple</h3>
15 <div style={{
16 fontSize: '48px',
17 fontWeight: 'bold',
18 margin: '20px 0',
19 color: count > 0 ? '#4caf50' : count < 0 ? '#f44336' : 'white'
20 }}>
21 {count}
22 </div>
23 <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
24 <button
25 onClick={() => setCount(count - 1)}
26 style={{
27 padding: '10px 20px',
28 backgroundColor: '#f44336',
29 color: 'white',
30 border: 'none',
31 borderRadius: '8px',
32 cursor: 'pointer',
33 fontSize: '18px'
34 }}
35 >
36 -1
37 </button>
38 <button
39 onClick={() => setCount(0)}
40 style={{
41 padding: '10px 20px',
42 backgroundColor: '#666',
43 color: 'white',
44 border: 'none',
45 borderRadius: '8px',
46 cursor: 'pointer'
47 }}
48 >
49 Reset
50 </button>
51 <button
52 onClick={() => setCount(count + 1)}
53 style={{
54 padding: '10px 20px',
55 backgroundColor: '#4caf50',
56 color: 'white',
57 border: 'none',
58 borderRadius: '8px',
59 cursor: 'pointer',
60 fontSize: '18px'
61 }}
62 >
63 +1
64 </button>
65 </div>
66 </div>
67 );
68}
69
70// Interrupteur - état booléen
71function ToggleSwitch() {
72 const [isOn, setIsOn] = useState(false);
73
74 return (
75 <div style={{
76 backgroundColor: '#252545',
77 padding: '20px',
78 borderRadius: '12px',
79 textAlign: 'center'
80 }}>
81 <h3 style={{ color: '#00d4ff', marginTop: 0 }}>Interrupteur</h3>
82 <div
83 onClick={() => setIsOn(!isOn)}
84 style={{
85 width: '80px',
86 height: '40px',
87 backgroundColor: isOn ? '#4caf50' : '#666',
88 borderRadius: '20px',
89 cursor: 'pointer',
90 position: 'relative',
91 margin: '20px auto',
92 transition: 'background-color 0.3s'
93 }}
94 >
95 <div style={{
96 width: '32px',
97 height: '32px',
98 backgroundColor: 'white',
99 borderRadius: '50%',
100 position: 'absolute',
101 top: '4px',
102 left: isOn ? '44px' : '4px',
103 transition: 'left 0.3s'
104 }} />
105 </div>
106 <p style={{ color: '#ccc' }}>
107 Statut : <strong style={{ color: isOn ? '#4caf50' : '#f44336' }}>
108 {isOn ? 'ACTIVÉ' : 'DÉSACTIVÉ'}
109 </strong>
110 </p>
111 </div>
112 );
113}
114
115// Formulaire - état de type chaîne
116function TextInput() {
117 const [text, setText] = useState('');
118
119 return (
120 <div style={{
121 backgroundColor: '#252545',
122 padding: '20px',
123 borderRadius: '12px'
124 }}>
125 <h3 style={{ color: '#00d4ff', marginTop: 0 }}>Champ de texte</h3>
126 <input
127 type="text"
128 value={text}
129 onChange={(e) => setText(e.target.value)}
130 placeholder="Écris quelque chose..."
131 style={{
132 width: '100%',
133 padding: '12px 15px',
134 border: '2px solid #444',
135 borderRadius: '8px',
136 backgroundColor: '#1a1a2e',
137 color: 'white',
138 fontSize: '16px',
139 boxSizing: 'border-box'
140 }}
141 />
142 <p style={{ color: '#888', marginBottom: 0 }}>
143 Saisi : <strong style={{ color: 'white' }}>{text || '(vide)'}</strong>
144 {' '}({text.length} caractères)
145 </p>
146 </div>
147 );
148}
149
150// Liste - état de type tableau
151function TodoList() {
152 const [todos, setTodos] = useState([
153 { id: 1, text: 'Apprendre React', done: true },
154 { id: 2, text: 'Comprendre useState', done: false },
155 { id: 3, text: 'Créer un projet', done: false }
156 ]);
157 const [newTodo, setNewTodo] = useState('');
158
159 const addTodo = () => {
160 if (newTodo.trim()) {
161 setTodos([...todos, {
162 id: Date.now(),
163 text: newTodo,
164 done: false
165 }]);
166 setNewTodo('');
167 }
168 };
169
170 const toggleTodo = (id) => {
171 setTodos(todos.map(todo =>
172 todo.id === id ? { ...todo, done: !todo.done } : todo
173 ));
174 };
175
176 const deleteTodo = (id) => {
177 setTodos(todos.filter(todo => todo.id !== id));
178 };
179
180 return (
181 <div style={{
182 backgroundColor: '#252545',
183 padding: '20px',
184 borderRadius: '12px'
185 }}>
186 <h3 style={{ color: '#00d4ff', marginTop: 0 }}>Liste de tâches</h3>
187
188 <div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
189 <input
190 type="text"
191 value={newTodo}
192 onChange={(e) => setNewTodo(e.target.value)}
193 onKeyDown={(e) => e.key === 'Enter' && addTodo()}
194 placeholder="Nouvelle tâche..."
195 style={{
196 flex: 1,
197 padding: '10px 15px',
198 border: '2px solid #444',
199 borderRadius: '8px',
200 backgroundColor: '#1a1a2e',
201 color: 'white'
202 }}
203 />
204 <button
205 onClick={addTodo}
206 style={{
207 padding: '10px 20px',
208 backgroundColor: '#4caf50',
209 color: 'white',
210 border: 'none',
211 borderRadius: '8px',
212 cursor: 'pointer'
213 }}
214 >
215 Ajouter
216 </button>
217 </div>
218
219 <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
220 {todos.map(todo => (
221 <div key={todo.id} style={{
222 display: 'flex',
223 alignItems: 'center',
224 gap: '10px',
225 padding: '10px 15px',
226 backgroundColor: '#1a1a2e',
227 borderRadius: '8px'
228 }}>
229 <input
230 type="checkbox"
231 checked={todo.done}
232 onChange={() => toggleTodo(todo.id)}
233 style={{ width: '20px', height: '20px', cursor: 'pointer' }}
234 />
235 <span style={{
236 flex: 1,
237 textDecoration: todo.done ? 'line-through' : 'none',
238 color: todo.done ? '#666' : 'white'
239 }}>
240 {todo.text}
241 </span>
242 <button
243 onClick={() => deleteTodo(todo.id)}
244 style={{
245 padding: '5px 10px',
246 backgroundColor: '#f44336',
247 color: 'white',
248 border: 'none',
249 borderRadius: '4px',
250 cursor: 'pointer'
251 }}
252 >
253 ×
254 </button>
255 </div>
256 ))}
257 </div>
258 <p style={{ color: '#888', marginBottom: 0, marginTop: '10px' }}>
259 Terminées : {todos.filter(t => t.done).length} / {todos.length}
260 </p>
261 </div>
262 );
263}
264
265export default function App() {
266 return (
267 <div style={{
268 fontFamily: 'Arial, sans-serif',
269 backgroundColor: '#0a0a1a',
270 minHeight: '100vh',
271 padding: '20px',
272 color: 'white'
273 }}>
274 <header style={{
275 textAlign: 'center',
276 marginBottom: '30px',
277 padding: '30px',
278 background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
279 borderRadius: '16px'
280 }}>
281 <h1 style={{ margin: 0 }}>useState dans React</h1>
282 <p style={{ margin: '10px 0 0 0', opacity: 0.9 }}>
283 Gestion de l'état des composants
284 </p>
285 </header>
286
287 <div style={{
288 display: 'grid',
289 gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
290 gap: '20px'
291 }}>
292 <SimpleCounter />
293 <ToggleSwitch />
294 <TextInput />
295 <TodoList />
296 </div>
297
298 <div style={{
299 backgroundColor: '#1a1a2e',
300 padding: '20px',
301 borderRadius: '12px',
302 marginTop: '30px'
303 }}>
304 <h3 style={{ color: '#ff9800', marginTop: 0 }}>Règles de useState :</h3>
305 <ul style={{ color: '#ccc', lineHeight: '1.8', paddingLeft: '20px' }}>
306 <li>À appeler uniquement au <strong>niveau supérieur</strong> du composant</li>
307 <li>Ne pas utiliser dans des boucles, des conditions ou des fonctions imbriquées</li>
308 <li>La mise à jour de l'état provoque un <strong>nouveau rendu</strong></li>
309 <li>L'état est <strong>isolé</strong> pour chaque instance du composant</li>
310 </ul>
311 </div>
312 </div>
313 );
314}Chacun des quatre panneaux a son propre état indépendant : cliquer sur le compteur ne modifie pas la liste de tâches, car chaque composant garde son état séparément.
Quand utiliser l'état ?
L'état doit être utilisé pour des données qui :
- Changent au fil du temps
- Influencent le rendu du composant
- Ne peuvent pas être calculées à partir des props ou d'un autre état
Toutes les données ne doivent pas être dans l'état. Voici de bonnes questions à te poser :
- La donnée provient-elle d'un parent via les props ? Si oui, elle ne devrait probablement pas être dans l'état.
- La donnée reste-t-elle inchangée au fil du temps ? Si oui, elle ne devrait probablement pas être dans l'état.
- La donnée peut-elle être calculée à partir de props ou d'un état existants ? Si oui, elle ne devrait surtout pas être dans l'état.
Résumé
L'état est un élément clé des composants React qui te permet de :
- Stocker des données propres au composant
- Gérer des données qui changent au fil du temps
- Créer des interfaces utilisateur interactives qui répondent aux actions de l'utilisateur
Dans les sections suivantes, nous verrons comment gérer les événements en React et comment les combiner avec la gestion de l'état pour créer des applications entièrement interactives.
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2
3function ControlPanel() {
4 const [fuel, setFuel] = useState(100);
5 const [speed, setSpeed] = useState(0);
6 const [shields, setShields] = useState(true);
7
8 return (
9 <div className="control-panel">
10 <h1>Tableau de bord du vaisseau spatial</h1>
11
12 <div className="gauges">
13 <div className="gauge">
14 <h3>Carburant</h3>
15 <div className="progress-bar">
16 <div
17 className="progress-fill fuel"
18 style={{ width: `${fuel}%` }}
19 >
20 {fuel}%
21 </div>
22 </div>
23 <button
24 onClick={() => setFuel(Math.max(0, fuel - 10))}
25 disabled={fuel <= 0}
26 >
27 Augmenter la poussée (-10% carburant)
28 </button>
29 </div>
30
31 <div className="gauge">
32 <h3>Vitesse</h3>
33 <p className="speed-display">{speed} km/s</p>
34 <div className="speed-controls">
35 <button onClick={() => setSpeed(speed + 1)}>
36 Accélérer (+1)
37 </button>
38 <button
39 onClick={() => setSpeed(Math.max(0, speed - 1))}
40 disabled={speed <= 0}
41 >
42 Ralentir (-1)
43 </button>
44 </div>
45 </div>
46
47 <div className="gauge">
48 <h3>Boucliers</h3>
49 <p className={`shield-status ${shields ? 'active' : 'inactive'}`}>
50 {shields ? 'Activés' : 'Désactivés'}
51 </p>
52 <button onClick={() => setShields(!shields)}>
53 {shields ? 'Désactiver les boucliers' : 'Activer les boucliers'}
54 </button>
55 </div>
56 </div>
57 </div>
58 );
59}
60
61function App() {
62 return (
63 <div className="app">
64 <ControlPanel />
65 </div>
66 );
67}
68
69export default App;Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Tri horizontal
Remets dans l'ordre la syntaxe de déclaration d'un état avec useState :