Maîtrisez le hook useFormState de React. Un guide complet pour une gestion d'état de formulaire simplifiée, la validation côté serveur et une expérience utilisateur améliorée avec les Server Actions.
React useFormState : Analyse approfondie de la gestion et de la validation modernes des formulaires
Les formulaires sont la pierre angulaire de l'interactivité sur le web. Des simples formulaires de contact aux assistants complexes à plusieurs étapes, ils sont essentiels pour la saisie des données par l'utilisateur et leur soumission. Pendant des années, les développeurs React ont navigué dans un paysage de solutions de gestion d'état, allant des simples hooks useState pour les scénarios de base aux bibliothèques tierces puissantes comme Formik et React Hook Form pour des besoins plus complexes. Bien que ces outils soient excellents, React évolue continuellement pour fournir des primitives plus intégrées et puissantes.
Voici useFormState, un hook introduit dans React 18. Initialement conçu pour fonctionner de manière transparente avec les React Server Actions, useFormState offre une approche simplifiée, robuste et native de la gestion de l'état des formulaires, en particulier lorsqu'il s'agit de logique et de validation côté serveur. Il simplifie le processus d'affichage des retours du serveur, tels que les erreurs de validation ou les messages de succès, directement dans votre interface utilisateur.
Ce guide complet vous propose une analyse approfondie du hook useFormState. Nous explorerons ses concepts fondamentaux, ses implémentations pratiques, ses modèles avancés et sa place dans l'écosystème plus large du développement React moderne. Que vous construisiez des applications avec Next.js, Remix ou React vanilla, la compréhension de useFormState vous dotera d'un outil puissant pour créer des formulaires meilleurs et plus résilients.
Qu'est-ce que `useFormState` et pourquoi en avons-nous besoin ?
À la base, useFormState est un hook conçu pour mettre à jour l'état en fonction du résultat d'une action de formulaire. Considérez-le comme une version spécialisée de useReducer, spécifiquement adaptée aux soumissions de formulaires. Il comble élégamment le fossé entre l'interaction de l'utilisateur côté client et le traitement côté serveur.
Avant useFormState, un flux de soumission de formulaire typique impliquant un serveur pouvait ressembler Ă ceci :
- L'utilisateur remplit un formulaire.
- L'état côté client (par exemple, en utilisant
useState) suit les valeurs des champs de saisie. - À la soumission, un gestionnaire d'événements (
onSubmit) empêche le comportement par défaut du navigateur. - Une requête
fetchest construite manuellement et envoyée à un point de terminaison d'API serveur. - Les états de chargement sont gérés (par exemple,
const [isLoading, setIsLoading] = useState(false)). - Le serveur traite la requête, effectue la validation et interagit avec une base de données.
- Le serveur renvoie une réponse JSON (par exemple,
{ success: false, errors: { email: 'Format invalide' } }). - Le code côté client analyse cette réponse et met à jour une autre variable d'état pour afficher les erreurs ou les messages de succès.
Ce processus, bien que fonctionnel, implique une quantité considérable de code répétitif (boilerplate) pour gérer les états de chargement, les états d'erreur et le cycle requête/réponse. useFormState, surtout lorsqu'il est associé aux Server Actions, simplifie considérablement cela en créant un flux plus déclaratif et intégré.
Les principaux avantages de l'utilisation de useFormState sont :
- Intégration transparente avec le serveur : C'est la solution native pour gérer les réponses des Server Actions, faisant de la validation côté serveur un citoyen de première classe dans votre composant.
- Gestion d'état simplifiée : Il centralise la logique des mises à jour de l'état du formulaire, réduisant le besoin de multiples hooks
useStatepour les données, les erreurs et l'état de soumission. - Amélioration progressive : Les formulaires construits avec
useFormStateet les Server Actions peuvent fonctionner même si JavaScript est désactivé sur le client, car ils sont basés sur les soumissions de formulaires HTML standard. - Expérience utilisateur améliorée : Il facilite la fourniture d'un retour immédiat et contextuel à l'utilisateur, comme des erreurs de validation en ligne ou des messages de succès, directement après la soumission d'un formulaire.
Comprendre la signature du hook `useFormState`
Pour maîtriser le hook, décomposons d'abord sa signature et ses valeurs de retour. C'est plus simple qu'il n'y paraît à première vue.
const [state, formAction] = useFormState(action, initialState);
Paramètres :
action: C'est une fonction qui sera exécutée lorsque le formulaire est soumis. Cette fonction reçoit deux arguments : l'état précédent du formulaire et les données du formulaire soumises. Elle est censée retourner le nouvel état. Il s'agit généralement d'une Server Action, mais cela peut être n'importe quelle fonction.initialState: C'est la valeur que vous voulez que l'état de votre formulaire ait initialement, avant qu'aucune soumission n'ait eu lieu. Cela peut être n'importe quelle valeur sérialisable (chaîne de caractères, nombre, objet, etc.).
Valeurs de retour :
useFormState renvoie un tableau contenant exactement deux éléments :
state: L'état actuel du formulaire. Au premier rendu, ce sera leinitialStateque vous avez fourni. Après une soumission de formulaire, ce sera la valeur retournée par votre fonctionaction. Cet état est ce que vous utilisez pour afficher les retours d'interface utilisateur, tels que les messages d'erreur.formAction: Une nouvelle fonction d'action que vous passez à la propactionde votre élément<form>. Lorsque cette action est déclenchée (par une soumission de formulaire), React appellera votre fonctionactionoriginale avec l'état précédent et les données du formulaire, puis mettra à jour lestateavec le résultat.
Ce modèle peut vous sembler familier si vous avez utilisé useReducer. La fonction action est comme un réducteur, l'initialState est l'état initial, et React gère le dispatching pour vous lorsque le formulaire est soumis.
Un premier exemple pratique : un simple formulaire d'abonnement
Construisons un simple formulaire d'abonnement à une newsletter pour voir useFormState en action. Nous aurons un seul champ pour l'e-mail et un bouton de soumission. L'action serveur effectuera une validation de base pour vérifier si un e-mail est fourni et s'il est dans un format valide.
D'abord, définissons notre action serveur. Si vous utilisez Next.js, vous pouvez la placer dans le même fichier que votre composant en ajoutant la directive 'use server'; en haut de la fonction.
// Dans actions.js ou en haut de votre fichier de composant avec 'use server'
export async function subscribe(previousState, formData) {
const email = formData.get('email');
if (!email) {
return { message: 'L\'e-mail est requis.' };
}
// Une simple regex à des fins de démonstration
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) {
return { message: 'Veuillez saisir une adresse e-mail valide.' };
}
// Ici, vous enregistreriez normalement l'e-mail dans une base de données
console.log(`Abonnement avec l'e-mail : ${email}`);
// Simuler un délai
await new Promise(res => setTimeout(res, 1000));
return { message: 'Merci pour votre abonnement !' };
}
Maintenant, créons le composant client qui utilise cette action avec useFormState.
'use client';
import { useFormState } from 'react-dom';
import { subscribe } from './actions';
const initialState = {
message: null,
};
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
<h3>Abonnez-vous Ă notre newsletter</h3>
<div>
<label htmlFor="email">Adresse e-mail</label>
<input type="email" id="email" name="email" required />
</div>
<button type="submit">S'abonner</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
Analysons ce qui se passe :
- Nous importons
useFormStatedepuisreact-dom(attention : pasreact). - Nous définissons un objet
initialState. Cela garantit que notre variablestatea une structure cohérente dès le premier rendu. - Nous appelons
useFormState(subscribe, initialState). Cela lie l'état de notre composant à l'action serveursubscribe. - Le
formActionretourné est passé à la propactionde l'élément<form>. C'est la connexion magique. - Nous affichons le message de notre objet
statede manière conditionnelle. Au premier rendu,state.messageestnull, donc rien n'est affiché. - Lorsque l'utilisateur soumet le formulaire, React invoque
formAction. Cela déclenche notre action serveursubscribe. La fonctionsubscribereçoit lepreviousState(initialement, notreinitialState) et leformData. - L'action serveur exécute sa logique et retourne un nouvel objet d'état (par exemple,
{ message: 'L\'e-mail est requis.' }). - React reçoit ce nouvel état et re-rend le composant
SubscriptionForm. La variablestatecontient maintenant le nouvel objet, et notre paragraphe conditionnel affiche le message d'erreur ou de succès.
C'est incroyablement puissant. Nous avons mis en œuvre une boucle de validation client-serveur complète avec un minimum de code répétitif pour la gestion de l'état côté client.
Améliorer l'UX avec `useFormStatus`
Notre formulaire fonctionne, mais l'expérience utilisateur pourrait être meilleure. Lorsque l'utilisateur clique sur "S'abonner", le bouton reste actif, et il n'y a aucune indication visuelle que quelque chose se passe jusqu'à ce que le serveur réponde. C'est là que le hook useFormStatus entre en jeu.
Le hook useFormStatus fournit des informations sur l'état de la dernière soumission de formulaire. Point crucial, il doit être utilisé dans un composant qui est un enfant de l'élément <form>. Il ne fonctionnera pas s'il est appelé dans le même composant qui rend le formulaire.
Créons un composant SubmitButton distinct.
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Abonnement en cours...' : 'S\'abonner'}
</button>
);
}
Maintenant, nous pouvons mettre Ă jour notre SubscriptionForm pour utiliser ce nouveau composant :
// ... imports
import { SubmitButton } from './SubmitButton';
// ... initialState et autre code
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
{/* ... champs du formulaire ... */}
<SubmitButton /> {/* Remplacez l'ancien bouton */}
{state?.message && <p>{state.message}</p>}
</form>
);
}
Avec ce changement, lorsque le formulaire est soumis, la valeur pending de useFormStatus devient true. Notre composant SubmitButton se re-rend, désactivant le bouton et changeant son texte en "Abonnement en cours...". Une fois que l'action serveur est terminée et que useFormState met à jour l'état, le formulaire n'est plus en attente, et le bouton revient à son état d'origine. Cela fournit un retour essentiel à l'utilisateur et empêche les soumissions multiples.
Validation avancée avec des états d'erreur structurés et Zod
Une simple chaîne de caractères pour le message est suffisante pour les formulaires simples, mais les applications réelles nécessitent souvent des erreurs de validation par champ. Nous pouvons facilement y parvenir en retournant un objet d'état plus structuré depuis notre action serveur.
Améliorons notre action pour qu'elle retourne un objet avec une clé errors, qui contient elle-même des messages pour des champs spécifiques. C'est aussi l'occasion idéale d'introduire une bibliothèque de validation de schémas comme Zod pour une logique de validation plus robuste et maintenable.
Étape 1 : Installer Zod
npm install zod
Étape 2 : Mettre à jour l'action serveur
Nous allons créer un schéma Zod pour définir la structure attendue et les règles de validation de nos données de formulaire. Ensuite, nous utiliserons schema.safeParse() pour valider le formData entrant.
'use server';
import { z } from 'zod';
// Définir le schéma pour notre formulaire
const contactSchema = z.object({
name: z.string().min(2, { message: 'Le nom doit comporter au moins 2 caractères.' }),
email: z.string().email({ message: 'Adresse e-mail invalide.' }),
message: z.string().min(10, { message: 'Le message doit comporter au moins 10 caractères.' }),
});
export async function submitContactForm(previousState, formData) {
const validatedFields = contactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// Si la validation échoue, retourner les erreurs
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'La validation a échoué. Veuillez vérifier vos saisies.',
};
}
// Si la validation réussit, traiter les données
// Par exemple, envoyer un e-mail ou sauvegarder dans une base de données
console.log('Succès !', validatedFields.data);
// ... logique de traitement ...
// Retourner un état de succès
return {
errors: {},
message: 'Merci pour votre message ! Nous vous recontacterons bientĂ´t.',
};
}
Remarquez comment nous utilisons validatedFields.error.flatten().fieldErrors. C'est un utilitaire Zod pratique qui transforme l'objet d'erreur en une structure plus utilisable, comme : { name: ['Le nom doit comporter au moins 2 caractères.'], message: ['Le message est trop court'] }.
Étape 3 : Mettre à jour le composant client
Maintenant, nous allons mettre à jour notre composant de formulaire pour gérer cet état d'erreur structuré.
'use client';
import { useFormState } from 'react-dom';
import { submitContactForm } from './actions';
import { SubmitButton } from './SubmitButton'; // En supposant que nous avons un bouton de soumission
const initialState = {
message: null,
errors: {},
};
export function ContactForm() {
const [state, formAction] = useFormState(submitContactForm, initialState);
return (
<form action={formAction}>
<h2>Contactez-nous</h2>
<div>
<label htmlFor="name">Nom</label>
<input type="text" id="name" name="name" />
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email">E-mail</label>
<input type="email" id="email" name="email" />
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" />
{state.errors?.message && (
<p className="error">{state.errors.message[0]}</p>
)}
</div>
<SubmitButton />
{state.message && <p className="form-status">{state.message}</p>}
</form>
);
}
Ce modèle est incroyablement évolutif et robuste. Votre action serveur devient la source unique de vérité pour la logique de validation, et Zod fournit un moyen déclaratif et typé de définir ces règles. Le composant client devient simplement un consommateur de l'état fourni par useFormState, affichant les erreurs là où elles doivent être. Cette séparation des préoccupations rend le code plus propre, plus facile à tester et plus sécurisé, car la validation est toujours appliquée sur le serveur.
`useFormState` vs. les autres solutions de gestion de formulaires
Avec un nouvel outil vient la question : « Quand devrais-je l'utiliser à la place de ce que je connais déjà ? » Comparons useFormState à d'autres approches courantes.
`useFormState` vs. `useState`
- `useState` est parfait pour les formulaires simples, uniquement côté client, ou lorsque vous devez effectuer des interactions complexes en temps réel côté client (comme la validation en direct pendant que l'utilisateur tape) avant la soumission. Il vous donne un contrôle direct et granulaire.
- `useFormState` excelle lorsque l'état du formulaire est principalement déterminé par une réponse du serveur. Il est conçu pour le cycle requête/réponse de la soumission de formulaire et constitue le choix de prédilection lors de l'utilisation des Server Actions. Il élimine le besoin de gérer manuellement les appels fetch, les états de chargement et l'analyse des réponses.
`useFormState` vs. les bibliothèques tierces (React Hook Form, Formik)
Les bibliothèques comme React Hook Form et Formik sont des solutions matures et riches en fonctionnalités qui offrent une suite complète d'outils pour la gestion de formulaires. Elles fournissent :
- Une validation avancée côté client (souvent avec une intégration de schémas pour Zod, Yup, etc.).
- Une gestion d'état complexe pour les champs imbriqués, les tableaux de champs, et plus encore.
- Des optimisations de performance (par exemple, en isolant les re-renders aux seuls champs qui changent).
- Des utilitaires pour les composants contrôlés et l'intégration avec les bibliothèques d'interface utilisateur.
Alors, quand choisir l'un ou l'autre ?
- Choisissez
useFormStatelorsque :- Vous utilisez les React Server Actions et souhaitez une solution native et intégrée.
- Votre principale source de vérité pour la validation est le serveur.
- Vous appréciez l'amélioration progressive et souhaitez que vos formulaires fonctionnent sans JavaScript.
- La logique de votre formulaire est relativement simple et centrée sur le cycle soumission/réponse.
- Choisissez une bibliothèque tierce lorsque :
- Vous avez besoin d'une validation côté client étendue et complexe avec un retour immédiat (par exemple, validation au `blur` ou au `change`).
- Vous avez des formulaires très dynamiques (par exemple, ajout/suppression de champs, logique conditionnelle).
- Vous n'utilisez pas un framework avec des Server Actions et construisez votre propre couche de communication client-serveur avec des API REST ou GraphQL.
- Vous avez besoin d'un contrôle fin sur les performances et les re-renders dans de très grands formulaires.
Il est également important de noter que ces approches ne s'excluent pas mutuellement. Vous pouvez utiliser React Hook Form pour gérer l'état et la validation côté client de votre formulaire, puis utiliser son gestionnaire de soumission pour appeler une Server Action. Cependant, pour de nombreux cas d'utilisation courants, la combinaison de useFormState et des Server Actions offre une solution plus simple et plus élégante.
Meilleures pratiques et pièges courants
Pour tirer le meilleur parti de useFormState, considérez les meilleures pratiques suivantes :
- Gardez les actions ciblées : Votre fonction d'action de formulaire doit être responsable d'une seule chose : traiter la soumission du formulaire. Cela inclut la validation, la mutation des données (sauvegarde en base de données) et le retour du nouvel état. Évitez les effets de bord qui ne sont pas liés au résultat du formulaire.
- Définissez une structure d'état cohérente : Commencez toujours avec un
initialStatebien défini et assurez-vous que votre action retourne toujours un objet avec la même structure, même en cas de succès. Cela évite les erreurs d'exécution côté client lorsque vous essayez d'accéder à des propriétés commestate.errors. - Adoptez l'amélioration progressive : N'oubliez pas que les Server Actions fonctionnent sans JavaScript côté client. Concevez votre interface utilisateur pour gérer les deux scénarios avec élégance. Par exemple, assurez-vous que les messages de validation rendus par le serveur sont clairs, car l'utilisateur n'aura pas l'avantage d'un bouton désactivé sans JS.
- Séparez les préoccupations de l'interface utilisateur : Utilisez des composants comme notre
SubmitButtonpour encapsuler l'interface utilisateur dépendant de l'état. Cela garde votre composant de formulaire principal plus propre et respecte la règle selon laquelleuseFormStatusdoit être utilisé dans un composant enfant. - N'oubliez pas l'accessibilité : Lors de l'affichage des erreurs, utilisez des attributs ARIA comme
aria-invalidsur vos champs de saisie et associez les messages d'erreur à leurs entrées respectives en utilisantaria-describedbypour garantir que vos formulaires sont accessibles aux utilisateurs de lecteurs d'écran.
Piège courant : Utiliser useFormStatus dans le même composant
Une erreur fréquente consiste à appeler useFormStatus dans le même composant qui rend la balise <form>. Cela ne fonctionnera pas car le hook doit être à l'intérieur du contexte du formulaire pour accéder à son état. Extrayez toujours la partie de votre interface utilisateur qui a besoin de l'état (comme un bouton) dans son propre composant enfant.
Conclusion
Le hook useFormState, de concert avec les Server Actions, représente une évolution significative dans la manière dont nous gérons les formulaires dans React. Il pousse les développeurs vers un modèle de validation plus robuste et centré sur le serveur, tout en simplifiant la gestion de l'état côté client. En faisant abstraction des complexités du cycle de vie de la soumission, il nous permet de nous concentrer sur ce qui compte le plus : définir notre logique métier et construire une expérience utilisateur fluide.
Bien qu'il ne remplace pas les bibliothèques tierces complètes pour tous les cas d'utilisation, useFormState fournit une base puissante, native et progressivement améliorée pour la grande majorité des formulaires dans les applications web modernes. En maîtrisant ses modèles et en comprenant sa place dans l'écosystème React, vous pouvez créer des formulaires plus résilients, maintenables et conviviaux avec moins de code et une plus grande clarté.