Retour au cours

frontend / react

Formulaires contrôlés

Leçon 71 exercice

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 useRef plutôt que useState pour 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.

ApprocheSource de véritéRe-rendu à chaque frappeCas d'usage
Champ contrôlé (useState)State ReactOuiValidation en temps réel, formatage
Champ non contrôlé (useRef)DOM natifNonFormulaire 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.

jsx
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>
    );
}
jsx
// 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>
        </>
    );
}
jsx
// 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 + onChange reliés au state : jamais l'un sans l'autre.
  • La clé calculée [name]: valeur permet 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

1 disponible
1

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é.

Résoudre l’exercice →