React : Les formulaires et les inputs contrôlés
Le composant contrôlé, où React est la source de vérité de chaque champ : gérer plusieurs inputs avec un seul état, soumettre, valider pendant le rendu, et gérer cases à cocher et listes déroulantes.
React : Les formulaires et les inputs contrôlés
On a amorcé l'idée avec onChange : lire une saisie et la stocker dans le state. Un formulaire, c'est cette idée généralisée à plusieurs champs, avec la soumission et la validation. Tout repose sur un concept central : le composant contrôlé.
# L'input contrôlé : React, source de vérité
Un champ HTML garde normalement sa valeur tout seul, dans le DOM. En React, on préfère que ce soit le state qui pilote la valeur. On lie donc les deux :
function Champ() {
const [nom, setNom] = useState("");
return (
<input
value={nom} // le state impose la valeur
onChange={(e) => setNom(e.target.value)} // chaque frappe met à jour le state
/>
);
}function Champ() { const [nom, setNom] = useState(""); return ( <input value={nom} // le state impose la valeur onChange={(e) => setNom(e.target.value)} // chaque frappe met à jour le state /> );}La boucle : je tape → onChange met à jour le state → re-rendu → value affiche le nouveau state. Le champ ne peut plus afficher autre chose que le state : c'est ça, un composant contrôlé. Avantage : la valeur est toujours disponible, prête à être validée ou envoyée.
# Plusieurs champs : un seul objet d'état
Un useState par champ devient vite lourd. On regroupe tout dans un objet, avec un seul gestionnaire générique qui s'appuie sur l'attribut name de chaque input :
function Inscription() {
const [form, setForm] = useState({ nom: "", email: "" });
function gererChangement(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
return (
<form>
<input name="nom" value={form.nom} onChange={gererChangement} />
<input name="email" value={form.email} onChange={gererChangement} />
</form>
);
}function Inscription() { const [form, setForm] = useState({ nom: "", email: "" }); function gererChangement(e) { const { name, value } = e.target; setForm((prev) => ({ ...prev, [name]: value })); } return ( <form> <input name="nom" value={form.nom} onChange={gererChangement} /> <input name="email" value={form.email} onChange={gererChangement} /> </form> );}Deux points clés, directement hérités du state et des objets JS :
[name]est une clé calculée :name="email"met à jourform.email.- On ne mute pas l'objet :
{ ...prev, [name]: value }en crée une nouvelle version.
# Soumettre le formulaire
À la soumission, on bloque le rechargement de page natif avec e.preventDefault(), puis on lit le state - toutes les valeurs y sont déjà :
function gererEnvoi(e) {
e.preventDefault();
console.log(form); // { nom: "...", email: "..." }
// envoi vers une API, etc.
}
// <form onSubmit={gererEnvoi}> ... <button>Envoyer</button> </form>function gererEnvoi(e) { e.preventDefault(); console.log(form); // { nom: "...", email: "..." } // envoi vers une API, etc.} // <form onSubmit={gererEnvoi}> ... <button>Envoyer</button> </form>Pas besoin d'aller relire le DOM : le state est le contenu du formulaire.
# Valider... pendant le rendu
La validation d'un champ, c'est une valeur dérivée de l'état. On la calcule donc directement pendant le rendu - surtout pas dans un useEffect :
const emailValide = form.email.includes("@");
return (
<form onSubmit={gererEnvoi}>
<input name="email" value={form.email} onChange={gererChangement} />
{form.email !== "" && !emailValide && (
<p className="erreur">Email invalide</p>
)}
<button disabled={!emailValide}>Envoyer</button>
</form>
);const emailValide = form.email.includes("@"); return ( <form onSubmit={gererEnvoi}> <input name="email" value={form.email} onChange={gererChangement} /> {form.email !== "" && !emailValide && ( <p className="erreur">Email invalide</p> )} <button disabled={!emailValide}>Envoyer</button> </form>);emailValide est recalculé à chaque rendu, donc toujours à jour. Le message d'erreur et l'état du bouton en découlent par rendu conditionnel.
# Cases à cocher et listes déroulantes
Le principe « valeur pilotée par le state » reste le même, seule la propriété change :
// Case à cocher : on lit e.target.checked (un booléen)
<input
type="checkbox"
checked={accepteCGU}
onChange={(e) => setAccepteCGU(e.target.checked)}
/>
// Liste déroulante : value sur le <select>
<select value={pays} onChange={(e) => setPays(e.target.value)}>
<option value="fr">France</option>
<option value="be">Belgique</option>
</select>// Case à cocher : on lit e.target.checked (un booléen)<input type="checkbox" checked={accepteCGU} onChange={(e) => setAccepteCGU(e.target.checked)}/> // Liste déroulante : value sur le <select><select value={pays} onChange={(e) => setPays(e.target.value)}> <option value="fr">France</option> <option value="be">Belgique</option></select>Retiens : value pour du texte et les select, checked pour les cases.
# Nuance : contrôlé vs non contrôlé
À l'inverse du composant contrôlé, un composant non contrôlé laisse le DOM gérer la valeur ; on la relit au besoin via une référence (useRef) et on fixe une valeur initiale avec defaultValue. C'est plus léger pour un cas simple, et obligatoire pour un <input type="file"> (dont la valeur ne peut pas être imposée par React).
En pratique : le contrôlé est le choix par défaut (validation en direct, valeur toujours accessible) ; le non contrôlé reste une exception ponctuelle.
# À retenir
- Un input contrôlé lie
valueau state et le met à jour viaonChange: React est la source de vérité. - Plusieurs champs → un objet d'état + un gestionnaire générique basé sur
name(clé calculée, spread, jamais de mutation). - À la soumission :
e.preventDefault(), puis on lit directement le state. - La validation est une valeur dérivée : on la calcule pendant le rendu, pas dans un
useEffect. checkedpour les cases,valuepour le texte et lesselect.
Prochaine étape : quand plusieurs composants doivent partager le même état, on le fait remonter vers leur parent - la communication entre composants (lifting state up), puis le Context.