Hooks, évènements et formulaires
Introduction aux hooks
Un hook est une fonction fournie par React qui donne accès à certaines de ses capacités (état, effets, références, contexte) depuis un composant fonctionnel.
En semaine 2, vous avez déjà utilisé un hook : useState. Cette semaine, nous en ajoutons deux autres (useEffect, useRef), nous voyons comment gérer les évènements et les formulaires, et nous apprenons à écrire nos propres hooks.
Les règles des hooks
Avant d’aller plus loin, deux règles qu’il faut absolument respecter :
- Toujours appeler les hooks au niveau supérieur du composant. Jamais à l’intérieur d’une condition, d’une boucle, ou d’une fonction imbriquée.
- N’appeler les hooks que depuis un composant React ou un autre hook personnalisé. Jamais depuis une fonction JavaScript ordinaire.
Ces règles permettent à React d’associer correctement chaque appel de hook à la bonne « case mémoire » du composant.
Le cycle de vie d’un composant
Un composant passe par trois moments clés :
- Apparition (il est rendu pour la première fois et inséré dans le DOM).
- Mise à jour (ses props changent, ou son état interne change).
- Disparition (il est retiré du DOM).
Le hook useEffect permet de réagir à ces moments depuis un composant fonctionnel.
useState : un rappel
const [valeur, setValeur] = useState(valeurInitiale);valeurest l’état courant.setValeurpermet de le mettre à jour et déclenche un nouveau rendu.useState(...)est invoqué une fois, à l’apparition du composant.
Appelez setValeur dans un gestionnaire d’évènement ou dans un effet.
useEffect : les effets de bord
Un effet de bord est tout ce qui sort du « rendu pur » d’un composant : appel HTTP (fetch), abonnement à un évènement global, modification du titre de la page, minuterie, etc.
useEffect(() => { // code à exécuter après le rendu return () => { // fonction de nettoyage (optionnelle) };}, [dependances]);Le tableau de dépendances
C’est le point à bien comprendre. Il contrôle quand l’effet s’exécute :
| Forme | Exécution |
|---|---|
useEffect(() => {...}) | Après chaque rendu. ⚠️ Risque de boucle infinie si l’effet modifie l’état. |
useEffect(() => {...}, []) | Une seule fois, à l’apparition du composant. |
useEffect(() => {...}, [x, y]) | À l’apparition, puis chaque fois que x ou y change. |
Exemple : récupérer des données d’une API
import { useState, useEffect } from "react";
function DataFetcher() { const [data, setData] = useState(null); const [erreur, setErreur] = useState(null); const [chargement, setChargement] = useState(true);
useEffect(() => { setChargement(true); fetch("https://jsonplaceholder.typicode.com/todos/1") .then((response) => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }) .then((json) => setData(json)) .catch((err) => setErreur(err.message)) .finally(() => setChargement(false)); }, []); // s'exécute une seule fois à l'apparition
if (chargement) return <p>Chargement…</p>; if (erreur) return <p>Erreur : {erreur}</p>; return <pre>{JSON.stringify(data, null, 2)}</pre>;}
export default DataFetcher;Exemple
Données: Aucune donnée
La fonction de nettoyage
Si l’effet met en place quelque chose qui doit être défait (écouteur d’évènement, minuterie, abonnement), retournez une fonction de nettoyage :
useEffect(() => { const id = setInterval(() => console.log("tic"), 1000); return () => clearInterval(id); // nettoyage au démontage}, []);useRef : références mutables
useRef sert à conserver une valeur entre les rendus sans déclencher de nouveau rendu quand elle change. Deux usages principaux :
1. Référencer un élément du DOM
import { useRef, useEffect } from "react";
function ChampAutoFocus() { const inputRef = useRef(null);
useEffect(() => { inputRef.current.focus(); }, []);
return <input ref={inputRef} placeholder="Tapez ici…" />;}2. Conserver une valeur mutable
const compteurDeClics = useRef(0);
function handleClick() { compteurDeClics.current += 1; // mise à jour silencieuse : ne déclenche pas de nouveau rendu}useState ou useRef ?
| Critère | useState | useRef |
|---|---|---|
| Déclenche un rendu quand la valeur change | ✅ Oui | ❌ Non |
| La valeur doit s’afficher | ✅ | ❌ |
| Accès à un élément du DOM | ❌ | ✅ |
| Stocker une valeur sans l’afficher (minuterie, identifiant précédent…) | ❌ | ✅ |
Règle simple : si la valeur est affichée ou influence ce qui est affiché, c’est du useState. Sinon, c’est du useRef.
useContext : un aperçu
useContext permet de partager une valeur (thème, utilisateur connecté, langue) avec tous les composants descendants sans avoir à la passer en props à chaque niveau.
Gestion des évènements
En React, les évènements s’attachent via des attributs JSX en camelCase (onClick, onChange, onSubmit…) et la valeur est une fonction, pas une chaîne.
function Bouton() { function handleClick() { alert("Bouton cliqué !"); }
return <button onClick={handleClick}>Cliquez-moi</button>;}L’objet event
Chaque gestionnaire reçoit un objet event (souvent nommé e) qui contient des informations sur l’évènement :
function ChampRecherche() { function handleChange(e) { console.log(e.target.value); // valeur actuelle du champ console.log(e.target.name); // attribut name du champ }
return <input name="recherche" onChange={handleChange} />;}Méthodes utiles :
e.preventDefault(): empêche le comportement par défaut (p. ex. soumission d’un formulaire qui recharge la page).e.stopPropagation(): empêche l’évènement de remonter aux parents.
Passer des arguments à un gestionnaire
function BoutonMessage({ message }) { function afficher(msg) { alert(msg); }
// On encapsule dans une fonction fléchée pour différer l'appel return <button onClick={() => afficher(message)}>Afficher</button>;}Passer un gestionnaire à un enfant
C’est le pattern clé pour faire remonter une action d’un composant enfant vers un parent :
import { useState } from "react";
function Bouton({ onClick }) { return <button onClick={onClick}>Cliquez-moi</button>;}
function Parent() { const [compteur, setCompteur] = useState(0);
return ( <div> <h2>Compteur : {compteur}</h2> <Bouton onClick={() => setCompteur(prev => prev + 1)} /> </div> );}Le parent détient l’état et contrôle les changements ; l’enfant se contente de signaler « l’utilisateur a cliqué ». Cette séparation est essentielle dans une application React.
Hooks personnalisés
Un hook personnalisé est simplement une fonction JavaScript qui utilise d’autres hooks et dont le nom commence par use. On les écrit pour :
- Réutiliser une logique à travers plusieurs composants.
- Isoler une logique complexe hors du composant pour le rendre plus lisible.
Exemple : useClickOutside
Ce hook détecte un clic en dehors d’un élément donné. Très utile pour fermer une modale ou un menu déroulant.
import { useEffect } from "react";
function useClickOutside(ref, onOutsideClick) { useEffect(() => { function handleClick(event) { if (ref.current && !ref.current.contains(event.target)) { onOutsideClick(); } }
document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [ref, onOutsideClick]);}Utilisation dans une modale :
import { useRef } from "react";
function Modale({ onClose, enfants }) { const modaleRef = useRef(null); useClickOutside(modaleRef, onClose);
return ( <div ref={modaleRef} className="modale"> {enfants} <button onClick={onClose}>Fermer</button> </div> );}Gestion des formulaires
En React, on gère les formulaires de deux manières : contrôlée (l’état React est la source de vérité) ou non contrôlée (le DOM est la source de vérité).
Formulaire contrôlé
La valeur de chaque champ est stockée dans un useState et synchronisée avec le champ via value + onChange.
import { useState } from "react";
function Formulaire() { const [nom, setNom] = useState("");
function handleSubmit(e) { e.preventDefault(); alert(`Nom soumis : ${nom}`); }
return ( <form onSubmit={handleSubmit}> <label> Nom : <input type="text" value={nom} onChange={(e) => setNom(e.target.value)} /> </label> <button type="submit" disabled={nom.trim() === ""}> Envoyer </button> </form> );}Avantages : validation en direct, boutons activés/désactivés dynamiquement, mise en forme conditionnelle.
Plusieurs champs dans un seul état
function Formulaire() { const [formData, setFormData] = useState({ nom: "", courriel: "" });
function handleChange(e) { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }
function handleSubmit(e) { e.preventDefault(); console.log("Données soumises :", formData); }
return ( <form onSubmit={handleSubmit}> <input name="nom" value={formData.nom} onChange={handleChange} /> <input name="courriel" value={formData.courriel} onChange={handleChange} type="email" /> <button type="submit">Envoyer</button> </form> );}Formulaire non contrôlé
Les valeurs restent dans le DOM ; on les lit au moment de la soumission via useRef.
import { useRef } from "react";
function FormulaireNonControle() { const nomRef = useRef(null);
function handleSubmit(e) { e.preventDefault(); console.log("Nom :", nomRef.current.value); }
return ( <form onSubmit={handleSubmit}> <input type="text" ref={nomRef} /> <button type="submit">Envoyer</button> </form> );}Variante pratique : FormData
Plutôt que d’attacher un ref à chaque champ, on met un ref sur le formulaire et on lit toutes les valeurs d’un coup :
import { useRef } from "react";
function FormulaireNonControle() { const formRef = useRef(null);
function handleSubmit(e) { e.preventDefault(); const data = Object.fromEntries(new FormData(formRef.current)); console.log(data); // { nom: "...", courriel: "..." } }
return ( <form ref={formRef} onSubmit={handleSubmit}> <input name="nom" /> <input name="courriel" type="email" /> <button type="submit">Envoyer</button> </form> );}Contrôlé ou non contrôlé ?
| Utilisez contrôlé si… | Utilisez non contrôlé si… |
|---|---|
| Vous devez valider en direct | Vous lisez seulement à la soumission |
| Le contenu d’un champ dépend d’un autre | Le formulaire est simple et isolé |
| Vous voulez activer/désactiver le bouton selon la saisie | Vous intégrez une bibliothèque tierce qui gère le DOM |
En doute, partez sur contrôlé — c’est l’approche recommandée par React et de loin la plus fréquente.
Erreurs communes à éviter
| Erreur | Symptôme | Correction |
|---|---|---|
Oublier e.preventDefault() dans onSubmit | La page recharge à chaque soumission | Appeler e.preventDefault() en première ligne du handler |
| Oublier le tableau de dépendances | Boucle de rendus, avertissements | Ajouter [] ou les dépendances réelles |
Dépendances manquantes dans useEffect | Avertissement ESLint, comportement incohérent | Inclure toutes les variables du scope utilisées dans l’effet |
Modifier un état directement (formData.nom = "...") | L’UI ne se met pas à jour | Utiliser setFormData(prev => ({ ...prev, nom: "..." })) |
| Appeler un hook dans une condition | Erreur React ou bogues subtils | Hooks toujours au niveau supérieur du composant |
Oublier value= sur un champ contrôlé | Avertissement « controlled/uncontrolled » | Fournir value et onChange |
Nommer un hook personnalisé sans préfixe use | ESLint ne vérifie plus les règles des hooks | Renommer en useXxx |
Récapitulatif
- Un hook donne accès à l’état, aux effets, aux références et au contexte depuis un composant fonctionnel.
useStatepour les valeurs qui influencent l’affichage ;useRefpour les valeurs qui n’y participent pas (ou pour accéder au DOM).useEffectpour les effets de bord ; le tableau de dépendances contrôle précisément quand il s’exécute.- Les évènements s’attachent en camelCase (
onClick,onChange…) et reçoivent un objetevent. - Un hook personnalisé encapsule de la logique réutilisable ; son nom commence par
use. - Les formulaires contrôlés (état React +
value/onChange) sont l’approche par défaut.
Exercices
- Titre dynamique. Dans un composant
PageTitre, utilisezuseEffectpour mettre à jourdocument.titleselon une proptitre. Nettoyez proprement au démontage. - Barre de recherche. Créez un composant
BarreRecherchecontrôlé qui affiche la saisie en temps réel et propose un bouton « Effacer » pour vider le champ. Servira directement au projet 1. - Hook
useLocalStorage. Écrivez un hook personnalisé qui se comporte commeuseStatemais persiste la valeur danslocalStorage. Signature :const [valeur, setValeur] = useLocalStorage("cle", valeurInitiale);. - Modale d’abonnement. Créez un composant
ModaleAbonnementqui s’affiche au clic sur un lien « M’abonner », se ferme au clic en dehors (useClickOutside) ou à la toucheEscape. Servira directement au projet 3. - Formulaire de filtre. Créez un formulaire contrôlé avec trois champs (arrondissement, sujet, date) et un bouton « Appliquer les filtres » désactivé tant qu’aucun champ n’est rempli. Transmet les valeurs à un parent via une prop
onFiltrer.