Un formulaire doit dire s’il envoie, s’il a réussi ou si quelque chose a échoué. Avec React 19, useActionState permet de regrouper le résultat d’une action et son état d’attente. Il reste à décider ce que la personne voit dans chacun de ces cas.
Essaie d’abord le cas qui échoue
Écris un nom de projet et coche « Simuler une erreur ». Après l’envoi, le texte reste dans le champ. Décoche la case et réessaie. La réponse prend volontairement un peu de temps pour rendre l’attente visible. Tout se passe dans ton navigateur, sans requête ni sauvegarde.
Le composant utilisé dans la démo
Le premier argument de l’action reçoit l’état précédent. Le second reçoit FormData. Le troisième élément retourné par le hook, ici nommépending, sert à bloquer les contrôles pendant le traitement.
import { useLocale } from "../../lib/locale";
import { useActionState, useState } from "react";
type State = { message: string; invalid: boolean; received?: string };
export default function FormDemo() {
const { t, locale } = useLocale();
const [name, setName] = useState("");
const [simulateError, setSimulateError] = useState(false);
const [state, action, pending] = useActionState(
async (_previous: State, data: FormData): Promise<State> => {
const value = String(data.get("project") ?? "").trim();
if (value.length < 3) {
return { message: "Écris au moins 3 caractères.", invalid: true };
}
// Simulation locale. Aucun appel réseau et aucune sauvegarde.
await new Promise((resolve) => setTimeout(resolve, 800));
if (simulateError) {
return {
message: "L’envoi a échoué. Tu peux réessayer.",
invalid: false,
};
}
setName("");
return {
message: "",
received: value,
invalid: false,
};
},
{ message: "Aucune donnée n’est enregistrée.", invalid: false },
);
return (
<div className="form-demo">
<label className="form-demo-checkbox">
<input
type="checkbox"
name="simulate-error"
checked={simulateError}
onChange={(event) => setSimulateError(event.target.checked)}
disabled={pending}
/>
{t("Simuler une erreur")}
</label>
<form
action={action}
className="contents"
aria-label={t("Essayer le formulaire")}
>
<label htmlFor="demo-project">{t("Nom du projet")}</label>
<input
id="demo-project"
name="project"
value={name}
onChange={(event) => setName(event.target.value)}
disabled={pending}
maxLength={80}
aria-invalid={state.invalid}
aria-describedby="form-demo-status"
placeholder={t("Mon prochain projet")}
/>
<button type="submit" disabled={pending}>
{pending ? t("Envoi en cours…") : t("Essayer l’envoi")}
</button>
<p id="form-demo-status" role="status" aria-live="polite">
{pending
? t("La réponse arrive…")
: state.received
? locale === "en"
? `“${state.received}” was received in the demo.`
: `« ${state.received} » a bien été reçu dans la démo.`
: t(state.message)}
</p>
</form>
</div>
);
}
Ne vider le champ qu’après le succès
Le champ est contrôlé avec useState. La fonction ne remet son contenu à zéro qu’après une réussite. Sur une erreur, elle retourne un message et garde la saisie. La case de simulation est contrôlée elle aussi pour que son état ne change pas pendant le test. Ce réglage de la démo se trouve hors du formulaire. La remise à zéro native du formulaire ne le modifie donc pas.
Le résultat passe par une zone annoncée aux lecteurs d’écran. Un changement de couleur seul ne suffirait pas. Le libellé du bouton indique également que l’action est en cours, sans dépendre d’une animation.
Ce que le serveur doit encore faire
Pour brancher un vrai service, remplace la simulation par un appel à ta route serveur. Vérifie les données et les permissions sur le serveur, même si le navigateur a déjà fait une validation. Retourne un message compréhensible en cas d’échec et garde les détails techniques dans des journaux qui ne contiennent pas de données sensibles.
Un bouton désactivé limite les doubles clics. Il ne garantit pas qu’une opération sera exécutée une seule fois. Pour une création sensible, prévois une clé d’idempotence et une contrainte côté stockage. Deux onglets ou une requête rejouée peuvent contourner le bouton sans difficulté.
Avant de livrer, essaie une saisie vide, une valeur trop courte, un échec suivi d’une réussite et une navigation au clavier. Ces quelques cas disent déjà beaucoup sur la qualité du formulaire.