frontend / react
Formulaires contrôlés
Explication
Ce que vous allez apprendre
- Comprendre la différence entre un champ contrôlé et un champ non contrôlé
- Piloter la valeur d'un input via le state avec la paire
value+onChange - Gérer un formulaire à plusieurs champs avec un seul handler générique basé sur
name - Valider un formulaire côté client sans oublier que la validation serveur reste obligatoire
- Choisir
useRefplutôt queuseStatepour un champ simple à faible fréquence de lecture
Dans quel contexte ?
Sur la page d'inscription src/pages/SignupPage.jsx, le bouton "S'inscrire" doit rester désactivé tant que l'email n'est pas valide et que le mot de passe ne fait pas au moins 8 caractères — une validation en temps réel impossible à faire proprement avec un formulaire HTML classique. En rendant les champs contrôlés, chaque frappe met à jour le state, ce qui permet de recalculer la validité à chaque caractère tapé et d'activer le bouton immédiatement.
Étape 1 : comment fonctionne un champ en HTML classique
En HTML classique, un champ de formulaire garde sa propre valeur en mémoire, gérée par le navigateur lui-même.
Étape 2 : l'inversion proposée par React
En React, on préfère souvent inverser ce fonctionnement : c'est le state du composant qui devient la seule source de vérité, et le champ affiche simplement ce que le state lui dicte. On dit que le champ est "contrôlé".
Étape 3 : ce que ça permet de faire
Laisser le state piloter la valeur d'un champ offre des possibilités qu'un formulaire HTML classique n'a pas facilement : valider en temps réel, formater automatiquement, activer un bouton selon le contenu.
Étape 4 : les deux props qui vont toujours ensemble
Un champ contrôlé combine systématiquement deux choses : value, qui impose d'afficher exactement ce que contient le state, et onChange, qui met à jour ce state à chaque frappe.
Étape 5 : le piège si on n'associe pas les deux
Mettre value sans onChange rendrait le champ complètement figé, l'utilisateur ne pourrait plus rien taper — c'est l'erreur la plus fréquente chez les débutants.
Piège fréquent
<input value={valeurs.email} /> sans onChange fige le champ : React affiche React-warning "You provided a value prop without an onChange handler" et l'utilisateur ne peut plus rien saisir. Toujours coupler value et onChange, ou utiliser defaultValue pour un champ non contrôlé.
Étape 6 : gérer plusieurs champs sans dupliquer de code
Pour un formulaire avec plusieurs champs, un pattern courant utilise l'attribut name de chaque champ comme clé dans un objet de state unique, avec un seul handler générique.
| Approche | Source de vérité | Re-rendu à chaque frappe | Cas d'usage |
|---|---|---|---|
Champ contrôlé (useState) | State React | Oui | Validation en temps réel, formatage |
Champ non contrôlé (useRef) | DOM natif | Non | Formulaire simple, lu seulement au submit |
Un dernier point essentiel
Il reste une règle à ne jamais oublier : une validation côté client n'est jamais une garantie de sécurité, seulement un confort UX. Une vraie validation côté serveur reste toujours indispensable.
Et ensuite ?
Une fois les formulaires contrôlés maîtrisés, la prochaine étape explore un outil différent, la ref, pour accéder directement au DOM.
Commandes & code
Formulaires contrôlés
Un composant contrôlé garde le state React comme unique source de vérité pour la valeur d'un champ.
function FormulaireInscription() {
const [valeurs, setValeurs] = useState({ email: "", mdp: "", accepteCgu: false });
const [erreurs, setErreurs] = useState({});
function gererChangement(e) {
const { name, value, type, checked } = e.target;
setValeurs((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value, // clé calculée = name du champ
}));
}
function valider() {
const nouvellesErreurs = {};
if (!valeurs.email.includes("@")) nouvellesErreurs.email = "Email invalide";
if (valeurs.mdp.length < 8) nouvellesErreurs.mdp = "8 caractères minimum";
if (!valeurs.accepteCgu) nouvellesErreurs.accepteCgu = "Requis";
setErreurs(nouvellesErreurs);
return Object.keys(nouvellesErreurs).length === 0;
}
function gererSubmit(e) {
e.preventDefault();
if (valider()) {
console.log("Envoi :", valeurs);
}
}
return (
<form onSubmit={gererSubmit}>
<input
name="email"
type="email"
value={valeurs.email}
onChange={gererChangement}
aria-invalid={!!erreurs.email}
/>
{erreurs.email && <span role="alert">{erreurs.email}</span>}
<input
name="mdp"
type="password"
value={valeurs.mdp}
onChange={gererChangement}
/>
{erreurs.mdp && <span role="alert">{erreurs.mdp}</span>}
<label>
<input
name="accepteCgu"
type="checkbox"
checked={valeurs.accepteCgu}
onChange={gererChangement}
/>
J'accepte les CGU
</label>
<button type="submit">S'inscrire</button>
</form>
);
}// select et textarea contrôlés : même logique que les inputs
function ChampsDivers() {
const [pays, setPays] = useState("fr");
const [bio, setBio] = useState("");
return (
<>
<select value={pays} onChange={(e) => setPays(e.target.value)}>
<option value="fr">France</option>
<option value="be">Belgique</option>
</select>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
maxLength={280}
/>
<p>{bio.length}/280</p>
</>
);
}// Non contrôlé + useRef : alternative légitime pour des cas simples (moins de re-rendus)
import { useRef } from "react";
function FormulaireNonControle() {
const emailRef = useRef(null);
function gererSubmit(e) {
e.preventDefault();
console.log("Email :", emailRef.current.value); // lu au moment du submit, pas à chaque frappe
}
return (
<form onSubmit={gererSubmit}>
<input ref={emailRef} type="email" defaultValue="" />
<button type="submit">Envoyer</button>
</form>
);
}Résumé
- Un champ contrôlé a toujours
value+onChangereliés au state : jamais l'un sans l'autre. - La clé calculée
[name]: valeurpermet un seul handler générique pour tout un formulaire. - Le non-contrôlé (
ref+defaultValue) réduit les re-rendus au prix de la réactivité en temps réel. - Valider côté client n'exempte jamais d'une validation serveur : c'est un confort UX, pas une sécurité.
Exercices pratiques
Mission : débloquer le champ email figé de SignupPage
Objectif : Réparer un champ contrôlé figé faute de handler, et factoriser un formulaire multi-champs avec un seul handler générique.
Contexte
Sur SignupPage.jsx, les utilisateurs signalent qu'ils ne peuvent taper aucun caractère dans le champ email : <input name="email" value={valeurs.email} />. Le formulaire compte aussi un champ mot de passe et une checkbox CGU, chacun avec son propre onChange dupliqué (setValeurs({...valeurs, mdp: e.target.value})), ce qui rend le fichier difficile à maintenir à chaque nouveau champ ajouté.