Cours JavaScript et React · Module 3: État et événements
Le flux de données
Dans cette leçon7
Lors d'un voyage cosmique à travers React, comprendre le flux de données revient à apprendre les cartes stellaires des systèmes - crucial pour une navigation sûre et efficace. React repose sur un flux de données unidirectionnel, qui garantit la prévisibilité et facilite le débogage des applications.
Le flux de données unidirectionnel
React utilise le concept de flux de données unidirectionnel (ou descendant, à sens unique). Cela signifie que les données circulent dans une seule direction : des composants parents vers les composants enfants.
Imagine la structure de ton application comme une chaîne de commandement dans une mission spatiale :
- Le centre de commandement de la mission (composant de plus haut niveau) envoie des instructions et des données aux différents modules
- Chaque module peut transmettre ces données à ses sous-modules
- Les sous-modules ne peuvent pas modifier directement les données reçues des modules parents
- Pour changer une donnée, les sous-modules doivent « rapporter » les changements aux modules parents, qui peuvent mettre à jour la donnée et la retransmettre
Les avantages du flux de données unidirectionnel
- Prévisibilité - il est plus facile de comprendre d'où viennent les données et comment elles changent
- Débogage plus facile - les problèmes de données peuvent être tracés « en amont » jusqu'à la source
- Meilleures performances - React peut optimiser les mises à jour des composants
- Moins de complexité - chaque composant a un rôle clairement défini dans le flux de données
Les props comme mécanisme de transmission des données vers le bas
Le principal mécanisme de transmission des données du parent vers les enfants, ce sont les props (propriétés) :
1// Composant parent
2function MissionControl() {
3 const missionData = {
4 name: "Apollo 11",
5 destination: "Lune",
6 crew: ["Neil Armstrong", "Buzz Aldrin", "Michael Collins"],
7 status: "En cours"
8 };
9
10 return (
11 <div className="mission-control">
12 <h1>Centre de contrôle de mission</h1>
13 <MissionInfo
14 name={missionData.name}
15 destination={missionData.destination}
16 status={missionData.status}
17 />
18 <CrewDisplay crew={missionData.crew} />
19 </div>
20 );
21}
22
23// Composant enfant
24function MissionInfo({ name, destination, status }) {
25 return (
26 <div className="mission-info">
27 <h2>{name}</h2>
28 <p>Destination : {destination}</p>
29 <p>Statut : {status}</p>
30 </div>
31 );
32}
33
34// Un autre composant enfant
35function CrewDisplay({ crew }) {
36 return (
37 <div className="crew-display">
38 <h2>Équipage</h2>
39 <ul>
40 {crew.map((member, index) => (
41 <li key={index}>{member}</li>
42 ))}
43 </ul>
44 </div>
45 );
46}Dans cet exemple :
MissionControlest le composant parent qui contient les données de la mission- Il transmet ces données vers le bas aux composants
MissionInfoetCrewDisplayvia les props - Les composants enfants reçoivent ces données et les affichent, mais ne peuvent pas les modifier directement
La communication ascendante - comment les enfants communiquent avec les parents
Puisque les données ne circulent que vers le bas, comment les composants de niveau inférieur peuvent-ils communiquer avec les composants de niveau supérieur ? La solution est une technique consistant à transmettre des fonctions vers le bas en tant que props, que les composants enfants peuvent appeler pour « rapporter » des changements au parent.
1function MissionControl() {
2 const [missionStatus, setMissionStatus] = useState("Préparation");
3
4 // Cette fonction sera transmise à l'enfant
5 const updateStatus = (newStatus) => {
6 setMissionStatus(newStatus);
7 console.log(`Statut de la mission mis à jour : ${newStatus}`);
8 };
9
10 return (
11 <div className="mission-control">
12 <h1>Centre de contrôle de mission</h1>
13 <p>Statut actuel : {missionStatus}</p>
14
15 {/* On passe la fonction updateStatus comme prop à StatusControls */}
16 <StatusControls
17 currentStatus={missionStatus}
18 onStatusChange={updateStatus}
19 />
20 </div>
21 );
22}
23
24function StatusControls({ currentStatus, onStatusChange }) {
25 return (
26 <div className="status-controls">
27 <h2>Contrôle du statut</h2>
28
29 <button
30 onClick={() => onStatusChange("Compte à rebours")}
31 disabled={currentStatus === "Compte à rebours"}
32 >
33 Lancer le compte à rebours
34 </button>
35
36 <button
37 onClick={() => onStatusChange("Lancement")}
38 disabled={currentStatus === "Lancement" || currentStatus !== "Compte à rebours"}
39 >
40 Lancer le décollage
41 </button>
42
43 <button
44 onClick={() => onStatusChange("Orbite")}
45 disabled={currentStatus === "Orbite" || currentStatus !== "Lancement"}
46 >
47 Confirmer la mise en orbite
48 </button>
49
50 <button
51 onClick={() => onStatusChange("Annulée")}
52 disabled={currentStatus === "Annulée" || currentStatus === "Orbite"}
53 >
54 Annuler la mission
55 </button>
56 </div>
57 );
58}Dans cet exemple :
MissionControl(parent) définit la fonctionupdateStatus- Il la transmet comme prop
onStatusChangeau composantStatusControls(enfant) StatusControlsappelle cette fonction avec une nouvelle valeur quand l'utilisateur clique sur un bouton- Une fois appelée, la fonction met à jour l'état dans le composant parent, déclenchant un nouveau rendu
- Les données mises à jour (le nouveau statut) redescendent vers les enfants via les props
Ce pattern préserve le flux de données unidirectionnel, même si les composants de niveau inférieur peuvent influencer les données des composants de niveau supérieur.
Faire remonter l'état (lifting state up)
« Faire remonter l'état » est un concept important en React, qui consiste à déplacer l'état des composants enfants vers le plus proche ancêtre commun qui a besoin d'accéder à ces données. Cette technique est cruciale pour maintenir le flux de données unidirectionnel.
Considère un exemple avec des modules de vaisseau spatial qui doivent partager des données :
1function SpaceshipDashboard() {
2 // L'état a été « remonté » vers le parent commun
3 const [power, setPower] = useState(100);
4 const [shields, setShields] = useState(75);
5 const [weapons, setWeapons] = useState(0);
6
7 // Fonctions pour mettre à jour l'état
8 const adjustPower = (system, amount) => {
9 if (system === 'shields') {
10 setShields(prev => Math.min(100, Math.max(0, prev + amount)));
11 setPower(prev => Math.max(0, prev - amount));
12 } else if (system === 'weapons') {
13 setWeapons(prev => Math.min(100, Math.max(0, prev + amount)));
14 setPower(prev => Math.max(0, prev - amount));
15 }
16 };
17
18 return (
19 <div className="spaceship-dashboard">
20 <h1>Panneau de commande du vaisseau</h1>
21
22 <PowerDisplay powerLevel={power} />
23
24 <div className="systems">
25 <ShieldSystem
26 shieldLevel={shields}
27 onAdjustPower={amount => adjustPower('shields', amount)}
28 />
29
30 <WeaponSystem
31 weaponCharge={weapons}
32 onAdjustPower={amount => adjustPower('weapons', amount)}
33 />
34 </div>
35 </div>
36 );
37}
38
39function PowerDisplay({ powerLevel }) {
40 return (
41 <div className="power-display">
42 <h2>Réserve d'énergie : {powerLevel}%</h2>
43 <div className="power-bar" style={{ width: `${powerLevel}%` }}></div>
44 </div>
45 );
46}
47
48function ShieldSystem({ shieldLevel, onAdjustPower }) {
49 return (
50 <div className="system-panel shields">
51 <h2>Boucliers : {shieldLevel}%</h2>
52 <div className="shield-bar" style={{ height: `${shieldLevel}%` }}></div>
53
54 <div className="controls">
55 <button onClick={() => onAdjustPower(10)} disabled={shieldLevel >= 100}>
56 + Augmenter l'énergie des boucliers
57 </button>
58 <button onClick={() => onAdjustPower(-10)} disabled={shieldLevel <= 0}>
59 - Réduire l'énergie des boucliers
60 </button>
61 </div>
62 </div>
63 );
64}
65
66function WeaponSystem({ weaponCharge, onAdjustPower }) {
67 return (
68 <div className="system-panel weapons">
69 <h2>Charge des armes : {weaponCharge}%</h2>
70 <div className="weapon-bar" style={{ height: `${weaponCharge}%` }}></div>
71
72 <div className="controls">
73 <button onClick={() => onAdjustPower(25)} disabled={weaponCharge >= 100}>
74 Charger les armes
75 </button>
76 <button onClick={() => onAdjustPower(-25)} disabled={weaponCharge <= 0}>
77 Décharger les armes
78 </button>
79 </div>
80 </div>
81 );
82}Dans cet exemple :
- L'état de l'énergie, des boucliers et des armes est stocké dans le composant parent
SpaceshipDashboard - Les composants
ShieldSystemetWeaponSystemreçoivent uniquement les données dont ils ont besoin et les fonctions pour mettre à jour ces données - Quand un système consomme de l'énergie, cela affecte l'énergie disponible pour les autres systèmes
- La logique de partage des ressources est gérée de manière centralisée par le composant parent
Cette technique est particulièrement utile quand :
- Plusieurs composants ont besoin des mêmes données
- Les données d'un composant affectent les données d'autres composants
- Les composants doivent être synchronisés
Les composants contrôlés et non contrôlés
En React, surtout dans le contexte des formulaires, il existe deux approches pour gérer les données saisies : les composants contrôlés et non contrôlés.
Les composants contrôlés
Dans les composants contrôlés, les données du formulaire sont gérées par l'état React. Chaque changement effectué par l'utilisateur passe par React :
1function ControlledMissionForm() {
2 const [formData, setFormData] = useState({
3 missionName: '',
4 duration: '',
5 crew: ''
6 });
7
8 const handleChange = (e) => {
9 setFormData({
10 ...formData,
11 [e.target.name]: e.target.value
12 });
13 };
14
15 const handleSubmit = (e) => {
16 e.preventDefault();
17 console.log('Données de la mission :', formData);
18 // Logique d'envoi des données...
19 };
20
21 return (
22 <form onSubmit={handleSubmit}>
23 <div>
24 <label>
25 Nom de la mission :
26 <input
27 type="text"
28 name="missionName"
29 value={formData.missionName}
30 onChange={handleChange}
31 />
32 </label>
33 </div>
34
35 <div>
36 <label>
37 Durée (jours) :
38 <input
39 type="number"
40 name="duration"
41 value={formData.duration}
42 onChange={handleChange}
43 />
44 </label>
45 </div>
46
47 <div>
48 <label>
49 Nombre de membres d'équipage :
50 <input
51 type="number"
52 name="crew"
53 value={formData.crew}
54 onChange={handleChange}
55 />
56 </label>
57 </div>
58
59 <button type="submit">Planifier la mission</button>
60 </form>
61 );
62}Avantages des composants contrôlés :
- Contrôle total sur l'état et validation en temps réel
- Possibilité de réagir immédiatement aux changements (par ex. validation dynamique)
- Facilité de manipulation et de transformation des données saisies
Les composants non contrôlés
Les composants non contrôlés laissent le DOM lui-même gérer l'état du formulaire :
1function UncontrolledMissionForm() {
2 // Références vers les éléments du DOM
3 const missionNameRef = useRef();
4 const durationRef = useRef();
5 const crewRef = useRef();
6
7 const handleSubmit = (e) => {
8 e.preventDefault();
9
10 const formData = {
11 missionName: missionNameRef.current.value,
12 duration: durationRef.current.value,
13 crew: crewRef.current.value
14 };
15
16 console.log('Données de la mission :', formData);
17 // Logique d'envoi des données...
18 };
19
20 return (
21 <form onSubmit={handleSubmit}>
22 <div>
23 <label>
24 Nom de la mission :
25 <input
26 type="text"
27 name="missionName"
28 ref={missionNameRef}
29 defaultValue="Mission Mars"
30 />
31 </label>
32 </div>
33
34 <div>
35 <label>
36 Durée (jours) :
37 <input
38 type="number"
39 name="duration"
40 ref={durationRef}
41 defaultValue="90"
42 />
43 </label>
44 </div>
45
46 <div>
47 <label>
48 Nombre de membres d'équipage :
49 <input
50 type="number"
51 name="crew"
52 ref={crewRef}
53 defaultValue="4"
54 />
55 </label>
56 </div>
57
58 <button type="submit">Planifier la mission</button>
59 </form>
60 );
61}Avantages des composants non contrôlés :
- Moins de code pour les formulaires simples
- Moins de rendus (meilleures performances dans certains cas)
- Intégration plus facile avec des bibliothèques externes
Dans la plupart des cas, il est recommandé d'utiliser des composants contrôlés car ils offrent un meilleur contrôle et sont plus alignés avec la philosophie de React. Cependant, dans certains scénarios (par ex. l'envoi de fichiers, l'intégration avec des bibliothèques externes), les composants non contrôlés peuvent être plus pratiques.
Le flux de données dans les applications plus importantes
Au fur et à mesure qu'une application grandit, le flux de données peut devenir plus complexe. Dans ces cas, il vaut la peine d'envisager :
1. Le Context (Context API)
Pour les données nécessaires à de nombreux endroits de l'application, tu peux utiliser le Context API pour éviter le « props drilling » (le fait de transmettre des props à travers de nombreux niveaux de composants) :
1// Création d'un contexte
2const MissionContext = React.createContext();
3
4// Composant Provider à un niveau élevé
5function MissionProvider({ children }) {
6 const [missionData, setMissionData] = useState({
7 name: "Artemis",
8 phase: "Préparation",
9 crew: ["Anna", "Jan", "Maria", "Piotr"],
10 launchDate: "2025-07-20"
11 });
12
13 const updateMission = (updates) => {
14 setMissionData(prev => ({
15 ...prev,
16 ...updates
17 }));
18 };
19
20 return (
21 <MissionContext.Provider value={{ missionData, updateMission }}>
22 {children}
23 </MissionContext.Provider>
24 );
25}
26
27// Utilisation dans un composant profondément imbriqué
28function DeepNestedComponent() {
29 const { missionData, updateMission } = useContext(MissionContext);
30
31 return (
32 <div>
33 <h3>Phase de la mission : {missionData.phase}</h3>
34 <button onClick={() => updateMission({ phase: "Compte à rebours" })}>
35 Lancer le compte à rebours
36 </button>
37 </div>
38 );
39}
40
41// Structure de l'application
42function App() {
43 return (
44 <MissionProvider>
45 <div className="app">
46 <Header />
47 <MainDashboard />
48 <Footer />
49 </div>
50 </MissionProvider>
51 );
52}2. La gestion d'état globale
Pour des applications encore plus complexes, tu peux envisager des bibliothèques de gestion d'état comme Redux Toolkit, Zustand ou MobX. Elles offrent des mécanismes plus avancés pour gérer l'état global.
Résumé
Comprendre le flux de données en React est le fondement de la création d'applications prévisibles et maintenables :
- Flux de données unidirectionnel - les données circulent de haut en bas, des parents vers les enfants
- Props - le principal mécanisme de transmission des données vers le bas
- Fonctions comme props - le mécanisme de communication ascendante
- Faire remonter l'état (lifting state up) - la technique consistant à déplacer un état partagé vers un ancêtre commun
- Composants contrôlés vs non contrôlés - différentes approches de la gestion des données de formulaire
- Context API et bibliothèques de gestion d'état - des solutions pour les flux de données plus complexes
En suivant ces principes, tu crées des applications plus faciles à comprendre, à déboguer et à faire évoluer - tout comme une mission spatiale bien planifiée a des protocoles de communication et un flux de données clairement définis.
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2
3// Libellés affichés : les valeurs des statuts et les classes CSS restent sans accents
4const STATUS_LABELS = {
5 planifiee: 'planifiée',
6 'en cours': 'en cours',
7 terminee: 'terminée',
8 annulee: 'annulée'
9};
10
11// Composant enfant - reçoit les données via les props
12function MissionInfo({ mission, onStatusChange }) {
13 return (
14 <div className="mission-info">
15 <h3>{mission.name}</h3>
16 <p>Statut : <span className={`status ${mission.status}`}>{STATUS_LABELS[mission.status]}</span></p>
17 <p>Cible : {mission.destination}</p>
18 <p>Équipage : {mission.crew.join(', ')}</p>
19
20 <div className="controls">
21 <button
22 onClick={() => onStatusChange(mission.id, 'en cours')}
23 disabled={mission.status === 'en cours'}
24 >
25 Démarrer la mission
26 </button>
27 <button
28 onClick={() => onStatusChange(mission.id, 'terminee')}
29 disabled={mission.status === 'terminee'}
30 >
31 Terminer la mission
32 </button>
33 <button
34 onClick={() => onStatusChange(mission.id, 'annulee')}
35 disabled={mission.status === 'annulee'}
36 >
37 Annuler la mission
38 </button>
39 </div>
40 </div>
41 );
42}
43
44// Composant parent - gère l'état et transmet les données vers le bas
45function MissionControl() {
46 const [missions, setMissions] = useState([
47 {
48 id: 1,
49 name: 'Apollo 21',
50 status: 'planifiee',
51 destination: 'Lune',
52 crew: ['Anna Kowalska', 'Jan Nowak']
53 },
54 {
55 id: 2,
56 name: 'Mars Explorer',
57 status: 'planifiee',
58 destination: 'Mars',
59 crew: ['Maria Wisniewska', 'Piotr Zielinski', 'Katarzyna Dabrowska']
60 },
61 {
62 id: 3,
63 name: 'Titan Survey',
64 status: 'en cours',
65 destination: 'Titan (lune de Saturne)',
66 crew: ['Tomasz Kowalczyk', 'Agnieszka Nowak']
67 }
68 ]);
69
70 // Fonction transmise vers le bas - communication ascendante
71 const handleStatusChange = (missionId, newStatus) => {
72 setMissions(prevMissions =>
73 prevMissions.map(mission =>
74 mission.id === missionId
75 ? { ...mission, status: newStatus }
76 : mission
77 )
78 );
79 };
80
81 const getStatusCount = (status) => {
82 return missions.filter(mission => mission.status === status).length;
83 };
84
85 return (
86 <div className="mission-control">
87 <h1>Centre de contrôle des missions</h1>
88
89 <div className="statistics">
90 <div className="stat">
91 <span className="count">{getStatusCount('planifiee')}</span>
92 <span className="label">Planifiées</span>
93 </div>
94 <div className="stat">
95 <span className="count">{getStatusCount('en cours')}</span>
96 <span className="label">En cours</span>
97 </div>
98 <div className="stat">
99 <span className="count">{getStatusCount('terminee')}</span>
100 <span className="label">Terminées</span>
101 </div>
102 <div className="stat">
103 <span className="count">{getStatusCount('annulee')}</span>
104 <span className="label">Annulées</span>
105 </div>
106 </div>
107
108 <div className="missions-grid">
109 {missions.map(mission => (
110 <MissionInfo
111 key={mission.id}
112 mission={mission}
113 onStatusChange={handleStatusChange}
114 />
115 ))}
116 </div>
117 </div>
118 );
119}
120
121function App() {
122 return (
123 <div className="app">
124 <MissionControl />
125 </div>
126 );
127}
128
129export default App;Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée un formulaire de connexion avec les états [username, setUsername] et [password, setPassword]. Les deux champs doivent être contrôlés, par ex. <input value={username} onChange={e => setUsername(e.target.value)} />, et le champ du mot de passe a type="password". Le formulaire a onSubmit={handleLogin}, et handleLogin appelle e.preventDefault(). Après l'envoi, affiche le message Bienvenue {username} (garde dans l'état le fait que le formulaire a été envoyé).