Aller au contenu

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 :

  1. 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.
  2. 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 :

  1. Apparition (il est rendu pour la première fois et inséré dans le DOM).
  2. Mise à jour (ses props changent, ou son état interne change).
  3. 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);
  • valeur est l’état courant.
  • setValeur permet 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 :

FormeExé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èreuseStateuseRef
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 directVous lisez seulement à la soumission
Le contenu d’un champ dépend d’un autreLe formulaire est simple et isolé
Vous voulez activer/désactiver le bouton selon la saisieVous 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

ErreurSymptômeCorrection
Oublier e.preventDefault() dans onSubmitLa page recharge à chaque soumissionAppeler e.preventDefault() en première ligne du handler
Oublier le tableau de dépendancesBoucle de rendus, avertissementsAjouter [] ou les dépendances réelles
Dépendances manquantes dans useEffectAvertissement ESLint, comportement incohérentInclure toutes les variables du scope utilisées dans l’effet
Modifier un état directement (formData.nom = "...")L’UI ne se met pas à jourUtiliser setFormData(prev => ({ ...prev, nom: "..." }))
Appeler un hook dans une conditionErreur React ou bogues subtilsHooks 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 useESLint ne vérifie plus les règles des hooksRenommer en useXxx

Récapitulatif

  1. Un hook donne accès à l’état, aux effets, aux références et au contexte depuis un composant fonctionnel.
  2. useState pour les valeurs qui influencent l’affichage ; useRef pour les valeurs qui n’y participent pas (ou pour accéder au DOM).
  3. useEffect pour les effets de bord ; le tableau de dépendances contrôle précisément quand il s’exécute.
  4. Les évènements s’attachent en camelCase (onClick, onChange…) et reçoivent un objet event.
  5. Un hook personnalisé encapsule de la logique réutilisable ; son nom commence par use.
  6. Les formulaires contrôlés (état React + value/onChange) sont l’approche par défaut.

Exercices

  1. Titre dynamique. Dans un composant PageTitre, utilisez useEffect pour mettre à jour document.title selon une prop titre. Nettoyez proprement au démontage.
  2. Barre de recherche. Créez un composant BarreRecherche contrôlé qui affiche la saisie en temps réel et propose un bouton « Effacer » pour vider le champ. Servira directement au projet 1.
  3. Hook useLocalStorage. Écrivez un hook personnalisé qui se comporte comme useState mais persiste la valeur dans localStorage. Signature : const [valeur, setValeur] = useLocalStorage("cle", valeurInitiale);.
  4. Modale d’abonnement. Créez un composant ModaleAbonnement qui s’affiche au clic sur un lien « M’abonner », se ferme au clic en dehors (useClickOutside) ou à la touche Escape. Servira directement au projet 3.
  5. 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.

Ressources