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

La gestion des événements

12 min de lecture
Dans cette leçon8

Lors d'un voyage cosmique à travers React, la gestion des événements est comme un système de contrôle de vol, qui te permet de répondre aux différentes actions de l'utilisateur. C'est un élément clé de la création d'interfaces interactives qui réagissent aux actions de l'utilisateur, comme les clics, la saisie de texte ou les mouvements de la souris.

Les bases de la gestion des événements en React

En React, la gestion des événements fonctionne de manière similaire au HTML standard, mais avec quelques différences importantes :

  1. Les noms d'événements utilisent la convention camelCase au lieu des minuscules :

    • HTML : onclick, onmouseover
    • React : onClick, onMouseOver
  2. En JSX, on passe une fonction comme gestionnaire d'événement, et non une chaîne de caractères :

    • HTML : <button onclick="handleClick()">Click</button>
    • React : <button onClick={handleClick}>Click</button>
  3. Pour empêcher le comportement par défaut, il faut explicitement appeler preventDefault :

    • HTML : <a href="#" onclick="return false">Click</a>
    • React : <a href="#" onClick={(e) => e.preventDefault()}>Click</a>

Répondre aux actions de l'utilisateur

Les événements sont la façon dont les composants React répondent aux actions de l'utilisateur. Voyons les types d'événements de base et comment les gérer.

Gérer les clics

L'événement le plus simple et le plus couramment utilisé est onClick :

1function LaunchButton() {
2  const handleLaunch = () => {
3    console.log('Lancement du compte à rebours !');
4    // Logique de lancement de la fusée...
5  };
6
7  return (
8    <button
9      className="launch-button"
10      onClick={handleLaunch}
11    >
12      Lancer le compte à rebours
13    </button>
14  );
15}

Les arguments dans les gestionnaires d'événements

Chaque gestionnaire d'événement reçoit un objet événement comme premier argument. Il contient des informations sur l'événement et offre des méthodes comme preventDefault() :

1function NavigationLink() {
2  const handleClick = (event) => {
3    event.preventDefault(); // Empêche l'action par défaut (par ex. la navigation vers une autre page)
4    console.log("Lien cliqué, mais la page ne s'est pas rechargée");
5
6    // Navigation personnalisée ou autre logique...
7  };
8
9  return (
10    <a
11      href="/destination"
12      onClick={handleClick}
13    >
14      Aller à la destination
15    </a>
16  );
17}

L'objet que React transmet au gestionnaire est un SyntheticEvent - une enveloppe autour de l'événement natif du navigateur. Il a la même interface (target, preventDefault(), stopPropagation()) et fonctionne de la même façon dans tous les navigateurs ; l'événement d'origine se trouve dans event.nativeEvent. L'ordre est donc le suivant : l'utilisateur clique, le navigateur crée l'événement, React l'enveloppe dans un SyntheticEvent, puis appelle ton gestionnaire.

Passer des arguments aux gestionnaires d'événements

Parfois, nous devons passer des arguments supplémentaires à un gestionnaire d'événement. Nous pouvons le faire de deux façons :

1. À l'aide d'une fonction fléchée

1function MissionControl({ missions }) {
2  const selectMission = (missionId, event) => {
3    console.log(`Mission sélectionnée, ID : ${missionId}`);
4    console.log('Événement :', event);
5    // Logique de sélection de mission...
6  };
7
8  return (
9    <div className="mission-control">
10      <h2>Missions disponibles</h2>
11      <ul>
12        {missions.map(mission => (
13          <li key={mission.id}>
14            {mission.name}
15            <button onClick={(e) => selectMission(mission.id, e)}>
16              Choisir
17            </button>
18          </li>
19        ))}
20      </ul>
21    </div>
22  );
23}

2. À l'aide de bind

1function MissionControl({ missions }) {
2  const selectMission = (missionId, event) => {
3    console.log(`Mission sélectionnée, ID : ${missionId}`);
4    // Logique de sélection de mission...
5  };
6
7  return (
8    <div className="mission-control">
9      <h2>Missions disponibles</h2>
10      <ul>
11        {missions.map(mission => (
12          <li key={mission.id}>
13            {mission.name}
14            <button onClick={selectMission.bind(null, mission.id)}>
15              Choisir
16            </button>
17          </li>
18        ))}
19      </ul>
20    </div>
21  );
22}

L'approche avec la fonction fléchée est plus courante et généralement plus lisible.

Les événements populaires en React

React prend en charge un large éventail d'événements, que l'on peut répartir en plusieurs catégories :

Les événements de souris

  • onClick - clic
  • onDoubleClick - double clic
  • onMouseEnter - le curseur entre dans un élément
  • onMouseLeave - le curseur quitte un élément
  • onMouseMove - le curseur se déplace au-dessus d'un élément

Les événements de formulaire

  • onChange - changement de la valeur d'un élément de formulaire
  • onSubmit - envoi du formulaire
  • onFocus - l'élément reçoit le focus
  • onBlur - l'élément perd le focus

Les événements de clavier

  • onKeyDown - une touche est enfoncée
  • onKeyPress - obsolète, utilise plutôt onKeyDown
  • onKeyUp - une touche est relâchée

Les événements tactiles (pour les appareils mobiles)

  • onTouchStart - le toucher commence
  • onTouchMove - mouvement pendant le toucher
  • onTouchEnd - le toucher se termine

Exemples de gestion de différents événements

Les événements de souris avec l'état

L'état mémorise si le curseur est au-dessus du panneau et si le panneau a été cliqué, et les événements onMouseEnter, onMouseLeave et onClick ne font que modifier cet état :

1function InteractivePanel() {
2  const [isHovered, setIsHovered] = useState(false);
3  const [isClicked, setIsClicked] = useState(false);
4
5  return (
6    <div
7      className={`panel ${isHovered ? 'hovered' : ''} ${isClicked ? 'clicked' : ''}`}
8      onMouseEnter={() => setIsHovered(true)}
9      onMouseLeave={() => {
10        setIsHovered(false);
11        setIsClicked(false);
12      }}
13      onClick={() => setIsClicked(!isClicked)}
14    >
15      <h3>Panneau interactif</h3>
16      <p>
17        {isHovered ? 'Curseur sur le panneau' : 'Survole le panneau'}
18        {isClicked ? ' - Panneau actif' : ' - Clique pour activer'}
19      </p>
20    </div>
21  );
22}

Les événements de clavier

Les touches se gèrent comme les clics - avec onKeyDown sur un élément, et event.key indique quelle touche a été enfoncée :

1function SpaceshipControls() {
2  const [position, setPosition] = useState({ x: 50, y: 50 });
3
4  const handleKeyDown = (event) => {
5    switch(event.key) {
6      case 'ArrowUp':
7        setPosition(prev => ({ ...prev, y: Math.max(0, prev.y - 5) }));
8        break;
9      case 'ArrowDown':
10        setPosition(prev => ({ ...prev, y: Math.min(100, prev.y + 5) }));
11        break;
12      case 'ArrowLeft':
13        setPosition(prev => ({ ...prev, x: Math.max(0, prev.x - 5) }));
14        break;
15      case 'ArrowRight':
16        setPosition(prev => ({ ...prev, x: Math.min(100, prev.x + 5) }));
17        break;
18      default:
19        return;
20    }
21  };
22
23  return (
24    <div className="spaceship-simulator" tabIndex={0} onKeyDown={handleKeyDown}>
25      <h2>Simulateur de vaisseau spatial</h2>
26      <p>Clique sur le simulateur et pilote le vaisseau avec les flèches</p>
27
28      <div className="universe" style={{ position: 'relative', width: '400px', height: '400px', background: 'black' }}>
29        <div
30          className="spaceship"
31          style={{
32            position: 'absolute',
33            left: `${position.x}%`,
34            top: `${position.y}%`,
35            transform: 'translate(-50%, -50%)',
36            width: '20px',
37            height: '20px',
38            background: 'red'
39          }}
40        ></div>
41      </div>
42
43      <div className="coordinates">
44        <p>Position X : {position.x}</p>
45        <p>Position Y : {position.y}</p>
46      </div>
47    </div>
48  );
49}

tabIndex={0} permet à l'élément div de recevoir le focus, donc onKeyDown fonctionne dès que tu cliques sur le simulateur. Écouter tout le document nécessite useEffect avec un nettoyage - tu le découvriras dans le module sur les effets.

Passer des fonctions comme props

Nous avons souvent besoin de transmettre des fonctions gestionnaires d'événements d'un composant parent à un composant enfant. C'est une pratique courante qui permet la communication entre les composants :

1function MissionControl({ onLaunch, onAbort }) {
2  const [countdown, setCountdown] = useState(10);
3  const [isLaunching, setIsLaunching] = useState(false);
4  // useRef conserve l'identifiant du timer entre les rendus (modifier une ref ne déclenche pas de nouveau rendu)
5  const timerRef = useRef(null);
6
7  const stopTimer = () => {
8    clearInterval(timerRef.current);
9    timerRef.current = null;
10  };
11
12  const startCountdown = () => {
13    setIsLaunching(true);
14    let secondsLeft = 10;
15    setCountdown(secondsLeft);
16
17    timerRef.current = setInterval(() => {
18      secondsLeft -= 1;
19      setCountdown(secondsLeft);
20      if (secondsLeft === 0) {
21        stopTimer();
22        setIsLaunching(false);
23        onLaunch(); // Appel de la fonction issue des props une fois le compte à rebours terminé (hors de la fonction de mise à jour de l'état)
24      }
25    }, 1000);
26  };
27
28  const abortLaunch = () => {
29    stopTimer(); // Arrête d'abord le compte à rebours
30    setIsLaunching(false);
31    setCountdown(10);
32    onAbort(); // Appel de la fonction issue des props en cas d'annulation
33  };
34
35  return (
36    <div className="mission-control">
37      <h2>Centre de contrôle de mission</h2>
38      <div className="countdown">
39        {isLaunching
40          ? `Lancement dans : ${countdown}`
41          : 'Prêt pour le lancement'}
42      </div>
43
44      <div className="control-buttons">
45        {!isLaunching ? (
46          <button onClick={startCountdown}>Lancer le compte à rebours</button>
47        ) : (
48          <button onClick={abortLaunch} className="abort">Annuler le lancement</button>
49        )}
50      </div>
51    </div>
52  );
53}
54
55// Utilisation du composant
56function SpaceProgram() {
57  const handleLaunch = () => {
58    console.log('La fusée a décollé !');
59    // Logique après le lancement...
60  };
61
62  const handleAbort = () => {
63    console.log('Lancement annulé !');
64    // Logique après l'annulation...
65  };
66
67  return (
68    <div className="space-program">
69      <h1>Programme spatial</h1>
70      <MissionControl
71        onLaunch={handleLaunch}
72        onAbort={handleAbort}
73      />
74    </div>
75  );
76}

La fonction passée à setState (mise à jour fonctionnelle) doit être pure : en mode développement (StrictMode), React l'appelle deux fois, donc y appeler onLaunch() lancerait la fusée deux fois. C'est pourquoi on compte les secondes dans une simple variable secondsLeft et on appelle onLaunch() et onAbort() directement dans le code du timer et du gestionnaire. L'identifiant du timer est conservé dans useRef pour que abortLaunch puisse l'arrêter - tu en sauras plus sur useRef avec les composants non contrôlés dans la leçon sur le flux de données.

La gestion des formulaires

Les formulaires sont un élément clé de l'interaction utilisateur dans les applications web. React propose deux approches pour la gestion des formulaires : les composants contrôlés et non contrôlés.

Les formulaires contrôlés

Dans les composants contrôlés, les données du formulaire sont contrôlées par l'état React :

1function MissionSetup() {
2  const [missionData, setMissionData] = useState({
3    name: '',
4    destination: 'Moon',
5    crew: 3,
6    duration: 7
7  });
8
9  const handleChange = (e) => {
10    const { name, value } = e.target;
11    setMissionData({
12      ...missionData,
13      [name]: value
14    });
15  };
16
17  const handleSubmit = (e) => {
18    e.preventDefault();
19    console.log('Données de la mission :', missionData);
20    // Logique d'envoi des données...
21  };
22
23  return (
24    <div className="mission-setup">
25      <h2>Configuration de la mission</h2>
26
27      <form onSubmit={handleSubmit}>
28        <div className="form-group">
29          <label htmlFor="name">Nom de la mission :</label>
30          <input
31            type="text"
32            id="name"
33            name="name"
34            value={missionData.name}
35            onChange={handleChange}
36            required
37          />
38        </div>
39
40        <div className="form-group">
41          <label htmlFor="destination">Destination :</label>
42          <select
43            id="destination"
44            name="destination"
45            value={missionData.destination}
46            onChange={handleChange}
47          >
48            <option value="Moon">Lune</option>
49            <option value="Mars">Mars</option>
50            <option value="ISS">Station spatiale internationale</option>
51            <option value="Asteroid">Ceinture d'astéroïdes</option>
52          </select>
53        </div>
54
55        <div className="form-group">
56          <label htmlFor="crew">Nombre de membres d'équipage :</label>
57          <input
58            type="number"
59            id="crew"
60            name="crew"
61            min="1"
62            max="10"
63            value={missionData.crew}
64            onChange={handleChange}
65          />
66        </div>
67
68        <div className="form-group">
69          <label htmlFor="duration">Durée (jours) :</label>
70          <input
71            type="number"
72            id="duration"
73            name="duration"
74            min="1"
75            value={missionData.duration}
76            onChange={handleChange}
77          />
78        </div>
79
80        <button type="submit">Planifier la mission</button>
81      </form>
82    </div>
83  );
84}

Dans l'exemple ci-dessus :

  1. Chaque champ du formulaire a une entrée correspondante dans l'état
  2. La valeur de chaque champ est contrôlée par l'état
  3. L'événement onChange met à jour l'état, qui à son tour met à jour la valeur du champ
  4. L'événement onSubmit sert à gérer l'envoi du formulaire

Gérer plusieurs types de champs de formulaire

Différents champs de formulaire nécessitent des approches légèrement différentes :

1function AstronautRegistration() {
2  const [formData, setFormData] = useState({
3    name: '',
4    age: '',
5    specialty: 'pilot',
6    experience: '',
7    isTrainee: false,
8    bio: '',
9    preferredShift: 'morning'
10  });
11
12  const handleChange = (e) => {
13    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
14    setFormData({
15      ...formData,
16      [e.target.name]: value
17    });
18  };
19
20  const handleSubmit = (e) => {
21    e.preventDefault();
22    console.log("Données de l'astronaute :", formData);
23    // Logique d'inscription...
24  };
25
26  return (
27    <div className="astronaut-registration">
28      <h2>Inscription des astronautes</h2>
29
30      <form onSubmit={handleSubmit}>
31        {/* Champ texte */}
32        <div className="form-group">
33          <label htmlFor="name">Nom complet :</label>
34          <input
35            type="text"
36            id="name"
37            name="name"
38            value={formData.name}
39            onChange={handleChange}
40            required
41          />
42        </div>
43
44        {/* Champ numérique */}
45        <div className="form-group">
46          <label htmlFor="age">Âge :</label>
47          <input
48            type="number"
49            id="age"
50            name="age"
51            value={formData.age}
52            onChange={handleChange}
53            required
54          />
55        </div>
56
57        {/* Select */}
58        <div className="form-group">
59          <label htmlFor="specialty">Spécialité :</label>
60          <select
61            id="specialty"
62            name="specialty"
63            value={formData.specialty}
64            onChange={handleChange}
65          >
66            <option value="pilot">Pilote</option>
67            <option value="engineer">Ingénieur</option>
68            <option value="scientist">Scientifique</option>
69            <option value="doctor">Médecin</option>
70          </select>
71        </div>
72
73        {/* Boutons radio */}
74        <div className="form-group">
75          <p>Équipe préférée :</p>
76          <label>
77            <input
78              type="radio"
79              name="preferredShift"
80              value="morning"
81              checked={formData.preferredShift === 'morning'}
82              onChange={handleChange}
83            />
84            Matin
85          </label>
86          <label>
87            <input
88              type="radio"
89              name="preferredShift"
90              value="evening"
91              checked={formData.preferredShift === 'evening'}
92              onChange={handleChange}
93            />
94            Soir
95          </label>
96        </div>
97
98        {/* Case à cocher */}
99        <div className="form-group">
100          <label>
101            <input
102              type="checkbox"
103              name="isTrainee"
104              checked={formData.isTrainee}
105              onChange={handleChange}
106            />
107            Statut de stagiaire
108          </label>
109        </div>
110
111        {/* Zone de texte */}
112        <div className="form-group">
113          <label htmlFor="bio">Biographie :</label>
114          <textarea
115            id="bio"
116            name="bio"
117            value={formData.bio}
118            onChange={handleChange}
119            rows="4"
120          ></textarea>
121        </div>
122
123        {/* Curseur (range) */}
124        <div className="form-group">
125          <label htmlFor="experience">Années d'expérience : {formData.experience}</label>
126          <input
127            type="range"
128            id="experience"
129            name="experience"
130            min="0"
131            max="30"
132            value={formData.experience}
133            onChange={handleChange}
134          />
135        </div>
136
137        <button type="submit">S'inscrire</button>
138      </form>
139    </div>
140  );
141}

La gestion des erreurs et la validation des formulaires

Dans les applications réelles, il est important de valider les données saisies par l'utilisateur :

1function MissionPlanner() {
2  const [formData, setFormData] = useState({
3    missionName: '',
4    launchDate: '',
5    budget: ''
6  });
7
8  const [errors, setErrors] = useState({});
9
10  const validateForm = () => {
11    const newErrors = {};
12
13    // Validation du nom de la mission
14    if (!formData.missionName.trim()) {
15      newErrors.missionName = 'Le nom de la mission est obligatoire';
16    } else if (formData.missionName.length < 3) {
17      newErrors.missionName = 'Le nom de la mission doit comporter au moins 3 caractères';
18    }
19
20    // Validation de la date de lancement
21    if (!formData.launchDate) {
22      newErrors.launchDate = 'La date de lancement est obligatoire';
23    } else {
24      const selectedDate = new Date(formData.launchDate);
25      const today = new Date();
26
27      if (selectedDate <= today) {
28        newErrors.launchDate = 'La date de lancement doit être dans le futur';
29      }
30    }
31
32    // Validation du budget
33    if (!formData.budget) {
34      newErrors.budget = 'Le budget est obligatoire';
35    } else if (isNaN(formData.budget) || Number(formData.budget) <= 0) {
36      newErrors.budget = 'Le budget doit être un nombre positif';
37    }
38
39    setErrors(newErrors);
40    return Object.keys(newErrors).length === 0; // Le formulaire est valide s'il n'y a aucune erreur
41  };
42
43  const handleChange = (e) => {
44    const { name, value } = e.target;
45    setFormData({
46      ...formData,
47      [name]: value
48    });
49
50    // Effacer l'erreur du champ en cours de modification
51    if (errors[name]) {
52      setErrors({
53        ...errors,
54        [name]: undefined
55      });
56    }
57  };
58
59  const handleSubmit = (e) => {
60    e.preventDefault();
61
62    if (validateForm()) {
63      console.log('Données de la mission envoyées :', formData);
64      // Logique d'envoi des données...
65    } else {
66      console.log('Le formulaire contient des erreurs');
67    }
68  };
69
70  return (
71    <div className="mission-planner">
72      <h2>Planification de la mission spatiale</h2>
73
74      <form onSubmit={handleSubmit}>
75        <div className="form-group">
76          <label htmlFor="missionName">Nom de la mission :</label>
77          <input
78            type="text"
79            id="missionName"
80            name="missionName"
81            value={formData.missionName}
82            onChange={handleChange}
83            className={errors.missionName ? 'error' : ''}
84          />
85          {errors.missionName && <div className="error-message">{errors.missionName}</div>}
86        </div>
87
88        <div className="form-group">
89          <label htmlFor="launchDate">Date de lancement :</label>
90          <input
91            type="date"
92            id="launchDate"
93            name="launchDate"
94            value={formData.launchDate}
95            onChange={handleChange}
96            className={errors.launchDate ? 'error' : ''}
97          />
98          {errors.launchDate && <div className="error-message">{errors.launchDate}</div>}
99        </div>
100
101        <div className="form-group">
102          <label htmlFor="budget">Budget (en millions de $) :</label>
103          <input
104            type="number"
105            id="budget"
106            name="budget"
107            value={formData.budget}
108            onChange={handleChange}
109            className={errors.budget ? 'error' : ''}
110          />
111          {errors.budget && <div className="error-message">{errors.budget}</div>}
112        </div>
113
114        <button type="submit">Planifier la mission</button>
115      </form>
116    </div>
117  );
118}

Résumé

La gestion des événements est un élément clé de la création d'applications interactives en React. Grâce à elle, nous pouvons :

  1. Répondre aux actions de l'utilisateur comme les clics, le mouvement de la souris ou la saisie de texte
  2. Mettre à jour l'état d'un composant en fonction des interactions
  3. Communiquer entre les composants en passant des fonctions comme props
  4. Gérer les formulaires et valider les données saisies par l'utilisateur

Une bonne gestion des événements, combinée à la gestion de l'état, te permet de créer des interfaces utilisateur dynamiques et réactives qui offrent aux utilisateurs une expérience intuitive.

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2
3function EventDemo() {
4  const [clickCount, setClickCount] = useState(0);
5  const [inputText, setInputText] = useState('');
6  const [isHovered, setIsHovered] = useState(false);
7  const [selectedOption, setSelectedOption] = useState('Mars');
8
9  const handleButtonClick = () => {
10    setClickCount(clickCount + 1);
11  };
12
13  const handleInputChange = (event) => {
14    setInputText(event.target.value);
15  };
16
17  const handleSubmit = (event) => {
18    event.preventDefault();
19    alert(`Formulaire envoyé avec le texte : "${inputText}"`);
20  };
21
22  const handleKeyDown = (event) => {
23    if (event.key === 'Enter') {
24      alert('Touche Entrée pressée !');
25    }
26  };
27
28  return (
29    <div className="event-demo">
30      <h1>Démo de gestion d'événements en React</h1>
31
32      <div className="demo-section">
33        <h2>Événements de souris</h2>
34        <button
35          onClick={handleButtonClick}
36          onMouseEnter={() => setIsHovered(true)}
37          onMouseLeave={() => setIsHovered(false)}
38          className={isHovered ? 'hovered' : ''}
39        >
40          Clique-moi ! (Cliqué : {clickCount} fois)
41        </button>
42      </div>
43
44      <div className="demo-section">
45        <h2>Événements de formulaire</h2>
46        <form onSubmit={handleSubmit}>
47          <input
48            type="text"
49            value={inputText}
50            onChange={handleInputChange}
51            onKeyDown={handleKeyDown}
52            placeholder="Saisis du texte et appuie sur Entrée ou envoie le formulaire"
53          />
54          <select
55            value={selectedOption}
56            onChange={(e) => setSelectedOption(e.target.value)}
57          >
58            <option value="Mars">Mars</option>
59            <option value="Lune">Lune</option>
60            <option value="Vénus">Vénus</option>
61            <option value="Jupiter">Jupiter</option>
62          </select>
63          <button type="submit">Envoyer le formulaire</button>
64        </form>
65
66        <div className="output">
67          <p>Texte actuel : {inputText}</p>
68          <p>Option sélectionnée : {selectedOption}</p>
69        </div>
70      </div>
71
72      <div className="demo-section">
73        <h2>Événements tactiles et d'interaction</h2>
74        <div
75          className="interactive-area"
76          onTouchStart={() => console.log('Toucher commencé')}
77          onTouchEnd={() => console.log('Toucher terminé')}
78          onDoubleClick={() => alert('Double clic !')}
79        >
80          Double-clique ou touche (vérifie la console)
81        </div>
82      </div>
83    </div>
84  );
85}
86
87function App() {
88  return (
89    <div className="app">
90      <EventDemo />
91    </div>
92  );
93}
94
95export 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 correcte pour mettre à jour l'état à partir de la valeur précédente :

  • Éditeur de code

    Crée un compteur d'oxygène : const [oxygen, setOxygen] = useState(21). Affiche le texte Niveau d'O2 : {oxygen}% et ajoute deux boutons : le bouton + augmente le niveau de 1 (<button onClick={() => setOxygen(prev => prev + 1)}>+</button>) et le bouton - le diminue de 1. Le niveau doit rester entre 0 et 100.

  • Clique dans l'ordre

    Construis l'attribut de gestion du clic correct en JSX :

  • Clique dans l'ordre

    Remets dans l'ordre les étapes d'utilisation de useState :

Articles utiles