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

La propagation des événements et les patterns avancés de gestion d'événements

7 min de lecture
Dans cette leçon7

À bord du vaisseau spatial React, les événements ne se déplacent pas dans le vide - ils voyagent à travers la hiérarchie des composants, comme un signal d'alarme qui se propage depuis un capteur de la salle des machines jusqu'au centre de commandement principal. Comprendre la propagation des événements te permet de contrôler avec précision quel composant répond au signal et quand.

Comment les événements se propagent en React

Dans le DOM du navigateur, les événements passent par trois phases :

  1. Capture - l'événement descend depuis la racine du document jusqu'à l'élément ciblé
  2. Cible - l'événement atteint l'élément qui a été cliqué
  3. Bubbling - l'événement remonte, de l'élément ciblé jusqu'à la racine

React gère les événements dans la phase de bubbling par défaut - ce qui signifie que l'événement « remonte » de l'enfant vers le parent. Imagine cela comme une alarme sur un vaisseau : le détecteur de fumée d'une cabine (élément ciblé) se déclenche, et le signal remonte - d'abord le commandant de pont l'entend, puis l'ingénieur en chef, et enfin le capitaine du vaisseau.

1function SpaceStation() {
2  const handleStationClick = () => {
3    console.log('3. Station spatiale : le signal a atteint le centre de commandement');
4  };
5
6  return (
7    <div onClick={handleStationClick} style={{ padding: '40px', background: '#1a1a3e' }}>
8      <h2>Station spatiale</h2>
9      <Deck />
10    </div>
11  );
12}
13
14function Deck() {
15  const handleDeckClick = () => {
16    console.log('2. Pont : le signal traverse le pont');
17  };
18
19  return (
20    <div onClick={handleDeckClick} style={{ padding: '30px', background: '#2a2a5e' }}>
21      <p>Pont principal</p>
22      <Cabin />
23    </div>
24  );
25}
26
27function Cabin() {
28  const handleCabinClick = () => {
29    console.log('1. Cabine : capteur activé !');
30  };
31
32  return (
33    <button onClick={handleCabinClick}>
34      Capteur de la cabine
35    </button>
36  );
37}

Après un clic sur le bouton « Capteur de la cabine », la console affichera :

  1. Cabine : capteur activé !
  2. Pont : le signal traverse le pont
  3. Station spatiale : le signal a atteint le centre de commandement

L'événement remonte de l'enfant vers le parent - exactement l'inverse de ce à quoi on pourrait s'attendre !

Arrêter la propagation - stopPropagation()

Parfois, tu veux que le signal ne quitte pas un module donné du vaisseau. La méthode e.stopPropagation() arrête le bubbling :

1function ControlPanel() {
2  const handlePanelClick = () => {
3    console.log('Panneau : ceci ne sera PAS appelé');
4  };
5
6  return (
7    <div onClick={handlePanelClick} style={{ padding: '20px' }}>
8      <h3>Panneau de commande</h3>
9      <EmergencyButton />
10    </div>
11  );
12}
13
14function EmergencyButton() {
15  const handleEmergency = (e) => {
16    e.stopPropagation(); // Arrête la propagation !
17    console.log("Bouton d'urgence : signal intercepté localement");
18  };
19
20  return (
21    <button onClick={handleEmergency}>
22      Bouton d'urgence
23    </button>
24  );
25}

Après un clic sur « Bouton d'urgence », seul le gestionnaire handleEmergency sera appelé - l'événement n'atteindra pas le parent ControlPanel.

Empêcher le comportement par défaut - preventDefault()

En plus de stopPropagation() (qui arrête le bubbling), il existe e.preventDefault() - qui empêche le comportement par défaut du navigateur. Ces deux méthodes font des choses complètement différentes :

MéthodeAction
e.stopPropagation()Arrête le bubbling vers les éléments parents
e.preventDefault()Bloque le comportement par défaut du navigateur (par ex. l'envoi du formulaire)
1function MissionForm() {
2  const [missionName, setMissionName] = useState('');
3
4  const handleSubmit = (e) => {
5    e.preventDefault(); // Empêche le rechargement de la page !
6    console.log('Mission soumise :', missionName);
7  };
8
9  return (
10    <form onSubmit={handleSubmit}>
11      <input
12        value={missionName}
13        onChange={(e) => setMissionName(e.target.value)}
14        placeholder="Nom de la mission"
15      />
16      <button type="submit">Soumettre la mission</button>
17    </form>
18  );
19}

Sans e.preventDefault(), le formulaire provoquerait un rechargement de la page - le comportement par défaut du navigateur pour l'élément <form>.

La délégation d'événements (Event Delegation)

La délégation d'événements est un pattern où un seul gestionnaire sur le parent gère les événements de tous les enfants. C'est comme un seul officier sur la passerelle qui gère tous les signaux des différents secteurs, au lieu d'affecter un officier distinct à chaque capteur :

1function CrewList() {
2  const crew = [
3    { id: 1, name: 'Capitaine Nova', role: 'Commandement' },
4    { id: 2, name: 'Ingénieur Astro', role: 'Propulsion warp' },
5    { id: 3, name: 'Navigateur Ra', role: 'Navigation' },
6    { id: 4, name: 'Médecin Luna', role: 'Infirmerie' },
7  ];
8
9  // Un seul gestionnaire sur le parent au lieu d'un par élément
10  const handleCrewClick = (e) => {
11    const crewId = e.target.closest('[data-crew-id]')?.dataset.crewId;
12    if (crewId) {
13      const member = crew.find(c => c.id === Number(crewId));
14      console.log("Membre d'équipage sélectionné :", member.name);
15    }
16  };
17
18  return (
19    <ul onClick={handleCrewClick}>
20      {crew.map(member => (
21        <li key={member.id} data-crew-id={member.id}>
22          {member.name} - {member.role}
23        </li>
24      ))}
25    </ul>
26  );
27}

En JavaScript pur, la délégation évite des centaines d'écouteurs distincts sur les grandes listes. React le fait pour toi : depuis la version 17, il attache un seul écouteur par type d'événement à la racine de l'application, donc un onClick sur 100 éléments <li> ne crée pas 100 écouteurs dans le DOM. En React, la délégation sert donc surtout quand un seul gestionnaire doit reconnaître l'élément cliqué (par ex. via un attribut data-*), pas pour les performances.

Passer des arguments aux gestionnaires

Un pattern courant consiste à passer des données supplémentaires à un gestionnaire d'événement. Tu peux envelopper l'appel dans une fonction fléchée dans le JSX ou préparer une fonction qui retourne un gestionnaire prêt à l'emploi (currying) :

1function MissionSelector() {
2  const missions = ['Apollo', 'Artemis', 'Voyager', 'Orion'];
3  const [selected, setSelected] = useState(null);
4
5  // Pattern : fonction qui retourne un gestionnaire
6  const handleSelect = (missionName) => () => {
7    setSelected(missionName);
8    console.log('Mission sélectionnée :', missionName);
9  };
10
11  // Autre possibilité, sans currying : const selectMission = (missionName) => setSelected(missionName);
12  // et dans le JSX : onClick={() => selectMission(mission)}
13
14  return (
15    <div>
16      <h3>Choisis une mission :</h3>
17      {missions.map(mission => (
18        <button key={mission} onClick={handleSelect(mission)}>
19          {mission}
20        </button>
21      ))}
22      {selected && <p>Mission active : {selected}</p>}
23    </div>
24  );
25}

Le pattern const handleSelect = (arg) => () => {...} (currying) n'est qu'une autre façon d'écrire la même chose : handleSelect(mission) crée lui aussi une nouvelle fonction pour chaque bouton à chaque rendu, tout comme onClick={() => selectMission(mission)}. Choisis l'écriture qui te paraît la plus lisible - créer des fonctions pendant le rendu est normal en React et il n'est pas nécessaire de l'éviter.

Gérer plusieurs événements avec un seul gestionnaire

Quand un composant gère plusieurs événements liés, tu peux utiliser un seul gestionnaire avec une différenciation par event.target.name ou event.target.dataset :

1function ShipControls() {
2  const [systems, setSystems] = useState({
3    engines: false,
4    shields: false,
5    weapons: false,
6  });
7
8  const handleToggle = (e) => {
9    const systemName = e.target.name;
10    setSystems(prev => ({
11      ...prev,
12      [systemName]: !prev[systemName]
13    }));
14  };
15
16  return (
17    <div>
18      <h3>Systèmes du vaisseau :</h3>
19      {Object.entries(systems).map(([name, isActive]) => (
20        <label key={name} style={{ display: 'block', margin: '8px 0' }}>
21          <input
22            type="checkbox"
23            name={name}
24            checked={isActive}
25            onChange={handleToggle}
26          />
27          {name}: {isActive ? 'ACTIF' : 'HORS LIGNE'}
28        </label>
29      ))}
30    </div>
31  );
32}

Grâce à [systemName]: !prev[systemName] (computed property name), un seul gestionnaire gère toutes les cases à cocher. L'attribut name de l'input identifie quel système doit être basculé.

Résumé

La propagation des événements et les patterns avancés de gestion d'événements sont des compétences clés pour un pilote de vaisseau React :

  1. Bubbling - les événements se propagent de l'enfant vers le parent (par défaut en React)
  2. stopPropagation() - arrête la propagation vers les éléments parents
  3. preventDefault() - bloque le comportement par défaut du navigateur (par ex. l'envoi d'un formulaire)
  4. Délégation d'événements - un seul gestionnaire sur le parent au lieu de plusieurs sur les enfants
  5. Passer des arguments aux gestionnaires - une fonction fléchée dans le JSX ou le currying
  6. Computed property names - un seul gestionnaire pour plusieurs éléments liés

Maîtriser ces techniques te permettra de construire des systèmes d'interaction précis où chaque signal sur le vaisseau va exactement là où il doit aller !

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2
3function EventLog({ logs }) {
4  return (
5    <div style={{
6      background: '#0d1117', borderRadius: '8px', padding: '12px',
7      maxHeight: '120px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '12px'
8    }}>
9      <div style={{ color: '#8b949e', marginBottom: '4px' }}>Console des événements :</div>
10      {logs.length === 0 && <div style={{ color: '#484f58' }}>Clique sur les éléments pour voir la propagation...</div>}
11      {logs.map((log, i) => (
12        <div key={i} style={{ color: log.color, padding: '2px 0' }}>
13          {log.text}
14        </div>
15      ))}
16    </div>
17  );
18}
19
20function SpaceStation() {
21  const [logs, setLogs] = useState([]);
22  const [stopPropagation, setStopPropagation] = useState(false);
23
24  const addLog = (text, color) => {
25    setLogs(prev => [...prev, { text, color }]);
26  };
27
28  const handleStationClick = () => {
29    addLog('3. Station spatiale : le signal a atteint le centre de commandement', '#f39c12');
30  };
31
32  const handleDeckClick = (e) => {
33    addLog('2. Pont principal : le signal traverse le pont', '#3498db');
34  };
35
36  const handleCabinClick = (e) => {
37    if (stopPropagation) {
38      e.stopPropagation();
39      addLog('1. Cabine : signal ARRÊTÉ (stopPropagation)', '#e74c3c');
40    } else {
41      addLog('1. Cabine : capteur activé !', '#2ecc71');
42    }
43  };
44
45  const handleClear = (e) => {
46    e.stopPropagation();
47    setLogs([]);
48  };
49
50  const styles = {
51    container: { background: '#0a0e1a', color: '#e0e8ff', fontFamily: 'sans-serif', padding: '20px', minHeight: '100vh' },
52    title: { color: '#4fc3f7', fontSize: '18px', marginBottom: '16px', textAlign: 'center' },
53    station: { background: '#1a1a3e', borderRadius: '12px', padding: '20px', border: '2px solid #4fc3f7', cursor: 'pointer' },
54    deck: { background: '#2a2a5e', borderRadius: '8px', padding: '16px', marginTop: '12px', border: '1px solid #6c6cae', cursor: 'pointer' },
55    cabin: {
56      display: 'block', width: '100%', marginTop: '12px', padding: '12px',
57      background: '#27ae60', color: 'white', border: 'none', borderRadius: '6px',
58      fontSize: '14px', cursor: 'pointer', fontWeight: 'bold'
59    },
60    toggle: {
61      display: 'flex', alignItems: 'center', gap: '8px',
62      margin: '16px 0', padding: '10px', background: '#1a2035', borderRadius: '6px'
63    },
64    clearBtn: {
65      background: '#333', color: '#aaa', border: '1px solid #555', borderRadius: '4px',
66      padding: '4px 12px', cursor: 'pointer', fontSize: '12px', marginTop: '8px'
67    }
68  };
69
70  return (
71    <div style={styles.container}>
72      <h2 style={styles.title}>Propagation des événements en React</h2>
73
74      <label style={styles.toggle}>
75        <input type="checkbox" checked={stopPropagation} onChange={() => setStopPropagation(prev => !prev)} />
76        <span style={{ color: stopPropagation ? '#e74c3c' : '#aaa' }}>
77          stopPropagation() {stopPropagation ? 'ACTIF' : 'désactivé'}
78        </span>
79      </label>
80
81      <div style={styles.station} onClick={handleStationClick}>
82        <div style={{ color: '#f39c12', fontSize: '12px' }}>onClick: Station spatiale</div>
83        <div style={styles.deck} onClick={handleDeckClick}>
84          <div style={{ color: '#3498db', fontSize: '12px' }}>onClick: Pont principal</div>
85          <button style={styles.cabin} onClick={handleCabinClick}>
86            Clique : Capteur de la cabine
87          </button>
88        </div>
89      </div>
90
91      <div style={{ marginTop: '16px' }}>
92        <EventLog logs={logs} />
93        <button style={styles.clearBtn} onClick={handleClear}>Effacer la console</button>
94      </div>
95    </div>
96  );
97}
98
99export default function App() {
100  return <SpaceStation />;
101}

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. Dans quel ordre les gestionnaires onClick sont-ils appelés lorsqu'on clique sur un élément imbriqué en React (par défaut) ?

  2. 2. Quelle est la différence entre e.stopPropagation() et e.preventDefault() ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée 3 états booléens : [lifeSupport, setLifeSupport] = useState(true), [powerGrid, setPowerGrid] = useState(true) et [communication, setCommunication] = useState(false). Pour chaque système, ajoute un bouton qui bascule son état (prev => !prev) et affiche le statut, par ex. Survie : {lifeSupport ? 'ON' : 'OFF'}. La couleur du statut dépend de l'état : vert pour ON, rouge pour OFF.

  • Tri vertical

    Remets dans l'ordre les phases de propagation d'un événement dans le navigateur, du début à la fin :

  • Tri horizontal

    Remets dans l'ordre la syntaxe pour arrêter la propagation d'un événement en React :

  • Éditeur de code

    Crée un composant MissionProtocols et affiche-le dans App. Garde dans l'état un tableau de protocoles { id, text, done }. Un champ texte contrôlé et un bouton Ajouter ajoutent un nouveau protocole (nouveau tableau avec spread). Chaque protocole a une case à cocher qui bascule done (map) et un bouton Supprimer qui retire uniquement ce protocole (filter). Mets en évidence les protocoles terminés avec un style, par ex. textDecoration: 'line-through'.

Articles utiles