Découvrez la puissance du hook useActionState de React. Apprenez comment il simplifie la gestion des formulaires, gère les états d'attente et améliore l'expérience utilisateur avec des exemples pratiques et détaillés.
React useActionState : Un guide complet sur la gestion moderne des formulaires
Le monde du développement web est en constante évolution, et l'écosystème React est à l'avant-garde de ce changement. Avec les versions récentes, React a introduit des fonctionnalités puissantes qui améliorent fondamentalement la façon dont nous construisons des applications interactives et résilientes. Parmi les plus marquantes se trouve le hook useActionState, qui change la donne pour la gestion des formulaires et des opérations asynchrones. Ce hook, anciennement connu sous le nom de useFormState dans les versions expérimentales, est maintenant un outil stable et essentiel pour tout développeur React moderne.
Ce guide complet vous plongera au cœur de useActionState. Nous explorerons les problèmes qu'il résout, ses mécanismes de base, et comment l'exploiter avec des hooks complémentaires comme useFormStatus pour créer des expériences utilisateur de qualité supérieure. Que vous construisiez un simple formulaire de contact ou une application complexe et riche en données, comprendre useActionState rendra votre code plus propre, plus déclaratif et plus robuste.
Le problème : La complexité de la gestion traditionnelle de l'état des formulaires
Avant de pouvoir apprécier l'élégance de useActionState, nous devons d'abord comprendre les défis qu'il relève. Pendant des années, la gestion de l'état des formulaires dans React a suivi un modèle prévisible mais souvent fastidieux utilisant le hook useState.
Prenons un scénario courant : un simple formulaire pour ajouter un nouveau produit à une liste. Nous devons gérer plusieurs éléments d'état :
- La valeur de l'input pour le nom du produit.
- Un état de chargement ou d'attente pour informer l'utilisateur pendant l'appel API.
- Un état d'erreur pour afficher des messages si la soumission échoue.
- Un état de succès ou un message à la fin de l'opération.
Une implémentation classique pourrait ressembler à ceci :
Exemple : L'« ancienne méthode » avec plusieurs hooks useState
// Fonction API fictive
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('Le nom du produit doit contenir au moins 3 caractères.');
}
console.log(`Produit \"${productName}\" ajouté.`);
return { success: true };
};
// Le composant
{error}import { useState } from 'react';
function OldProductForm() {
const [productName, setProductName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);
setError(null);
try {
await addProductAPI(productName);
setProductName(''); // Vider l'input en cas de succès
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};
return (
id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>
{isPending ? 'Ajout en cours...' : 'Ajouter le produit'}
{error &&
);
}
Cette approche fonctionne, mais elle présente plusieurs inconvénients :
- Code répétitif (Boilerplate) : Nous avons besoin de trois appels distincts à useState pour gérer ce qui est conceptuellement un seul processus de soumission de formulaire.
- Gestion manuelle de l'état : Le développeur est responsable de définir et réinitialiser manuellement les états de chargement et d'erreur dans le bon ordre au sein d'un bloc try...catch...finally. C'est répétitif et source d'erreurs.
- Couplage : La logique de gestion du résultat de la soumission du formulaire est fortement couplée à la logique de rendu du composant.
Présentation de useActionState : Un changement de paradigme
useActionState est un hook React conçu spécifiquement pour gérer l'état d'une action asynchrone, comme la soumission d'un formulaire. Il simplifie l'ensemble du processus en connectant directement l'état au résultat de la fonction d'action.
Sa signature est claire et concise :
const [state, formAction] = useActionState(actionFn, initialState);
Analysons ses composants :
actionFn(previousState, formData)
: C'est votre fonction asynchrone qui effectue le travail (par exemple, un appel API). Elle reçoit l'état précédent et les données du formulaire en arguments. Fait crucial, ce que cette fonction retourne devient le nouvel état.initialState
: C'est la valeur de l'état avant que l'action n'ait été exécutée pour la première fois.state
: C'est l'état actuel. Il contient la valeur initialState au début et est mis à jour avec la valeur de retour de votre actionFn après chaque exécution.formAction
: C'est une nouvelle version « encapsulée » de votre fonction d'action. Vous devez passer cette fonction à la propaction
de l'élément<form>
. React utilise cette fonction encapsulée pour suivre l'état d'attente de l'action.
Exemple pratique : Refactorisation avec useActionState
Maintenant, refactorisons notre formulaire de produit en utilisant useActionState. L'amélioration est immédiatement visible.
D'abord, nous devons adapter notre logique d'action. Au lieu de lancer des erreurs, l'action doit retourner un objet d'état qui décrit le résultat.
Exemple : La « nouvelle méthode » avec useActionState
// La fonction d'action, conçue pour fonctionner avec useActionState
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // Simule une latence réseau
if (!productName || productName.length < 3) {
return { message: 'Le nom du produit doit contenir au moins 3 caractères.', success: false };
}
console.log(`Produit \"${productName}\" ajouté.`);
// En cas de succès, retourner un message de succès et vider le formulaire.
return { message: `\"${productName}\" a été ajouté avec succès.`, success: true };
};
// Le composant refactorisé
{state.message} {state.message}import { useActionState } from 'react';
// Note : Nous ajouterons useFormStatus dans la section suivante pour gérer l'état d'attente.
function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
Regardez à quel point c'est plus propre ! Nous avons remplacé trois hooks useState par un seul hook useActionState. La responsabilité du composant est maintenant uniquement de rendre l'interface utilisateur en fonction de l'objet `state`. Toute la logique métier est soigneusement encapsulée dans la fonction `addProductAction`. L'état se met à jour automatiquement en fonction de ce que retourne l'action.
Mais attendez, qu'en est-il de l'état d'attente ? Comment désactiver le bouton pendant la soumission du formulaire ?
Gérer les états d'attente avec useFormStatus
React fournit un hook compagnon, useFormStatus, conçu pour résoudre exactement ce problème. Il fournit des informations sur l'état de la dernière soumission de formulaire, mais avec une règle essentielle : il doit être appelé depuis un composant qui est rendu à l'intérieur du <form>
dont vous voulez suivre l'état.
Cela encourage une séparation nette des responsabilités. Vous créez un composant spécifiquement pour les éléments d'interface qui doivent connaître l'état de soumission du formulaire, comme un bouton de soumission.
Le hook useFormStatus retourne un objet avec plusieurs propriétés, dont la plus importante est `pending`.
const { pending, data, method, action } = useFormStatus();
pending
: Un booléen qui est `true` si le formulaire parent est en cours de soumission et `false` sinon.data
: Un objet `FormData` contenant les données en cours de soumission.method
: Une chaîne de caractères indiquant la méthode HTTP (`'get'` ou `'post'`).action
: Une référence à la fonction passée à la prop `action` du formulaire.
Créer un bouton de soumission sensible à l'état
Créons un composant `SubmitButton` dédié et intégrons-le dans notre formulaire.
Exemple : Le composant SubmitButton
import { useFormStatus } from 'react-dom';
// Note : useFormStatus est importé de 'react-dom', pas de 'react'.
function SubmitButton() {
const { pending } = useFormStatus();
return (
{pending ? 'Ajout en cours...' : 'Ajouter le produit'}
);
}
Maintenant, nous pouvons mettre à jour notre composant de formulaire principal pour l'utiliser.
Exemple : Le formulaire complet avec useActionState et useFormStatus
{state.message} {state.message}import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (la fonction addProductAction reste la même)
function SubmitButton() { /* ... comme défini ci-dessus ... */ }
function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{/* On peut ajouter une clé pour réinitialiser l'input en cas de succès */}
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
Avec cette structure, le composant `CompleteProductForm` n'a pas besoin de connaître l'état d'attente. Le `SubmitButton` est entièrement autonome. Ce modèle de composition est incroyablement puissant pour construire des interfaces utilisateur complexes et maintenables.
La puissance de l'amélioration progressive
L'un des avantages les plus profonds de cette nouvelle approche basée sur les actions, en particulier lorsqu'elle est utilisée avec les Server Actions, est l'amélioration progressive automatique. C'est un concept vital pour construire des applications pour un public mondial, où les conditions de réseau peuvent être peu fiables et les utilisateurs peuvent avoir des appareils plus anciens ou JavaScript désactivé.
Voici comment cela fonctionne :
- Sans JavaScript : Si le navigateur d'un utilisateur n'exécute pas le JavaScript côté client, le `<form action={...}>` fonctionne comme un formulaire HTML standard. Il effectue une requête avec rechargement de page complet vers le serveur. Si vous utilisez un framework comme Next.js, l'action côté serveur s'exécute, et le framework effectue un nouveau rendu de la page entière avec le nouvel état (par exemple, en affichant l'erreur de validation). L'application est entièrement fonctionnelle, juste sans la fluidité d'une SPA.
- Avec JavaScript : Une fois que le bundle JavaScript est chargé et que React hydrate la page, la même `formAction` est exécutée côté client. Au lieu d'un rechargement de page complet, elle se comporte comme une requête fetch classique. L'action est appelée, l'état est mis à jour, et seules les parties nécessaires du composant sont re-rendues.
Cela signifie que vous écrivez votre logique de formulaire une seule fois, et elle fonctionne de manière transparente dans les deux scénarios. Vous construisez par défaut une application résiliente et accessible, ce qui est un gain énorme pour l'expérience utilisateur à travers le monde.
Patrons avancés et cas d'usage
1. Server Actions vs. Client Actions
La fonction `actionFn` que vous passez à useActionState peut être une fonction asynchrone standard côté client (comme dans nos exemples) ou une Server Action. Une Server Action est une fonction définie sur le serveur qui peut être appelée directement depuis des composants clients. Dans des frameworks comme Next.js, vous en définissez une en ajoutant la directive "use server";
en haut du corps de la fonction.
- Client Actions : Idéales pour les mutations qui n'affectent que l'état côté client ou qui appellent des API tierces directement depuis le client.
- Server Actions : Parfaites pour les mutations qui impliquent une base de données ou d'autres ressources côté serveur. Elles simplifient votre architecture en éliminant le besoin de créer manuellement des points de terminaison API pour chaque mutation.
La beauté de la chose est que useActionState fonctionne de manière identique avec les deux. Vous pouvez remplacer une action client par une action serveur sans changer le code du composant.
2. Mises à jour optimistes avec `useOptimistic`
Pour une sensation encore plus réactive, vous pouvez combiner useActionState avec le hook useOptimistic. Une mise à jour optimiste consiste à mettre à jour l'interface utilisateur immédiatement, en *supposant* que l'action asynchrone réussira. Si elle échoue, vous revenez à l'état précédent de l'interface.
Imaginez une application de réseau social où vous ajoutez un commentaire. De manière optimiste, vous afficheriez instantanément le nouveau commentaire dans la liste pendant que la requête est envoyée au serveur. useOptimistic est conçu pour fonctionner main dans la main avec les actions pour rendre ce patron simple à mettre en œuvre.
3. Réinitialiser un formulaire en cas de succès
Une exigence courante est de vider les champs du formulaire après une soumission réussie. Il y a plusieurs façons d'y parvenir avec useActionState.
- L'astuce de la prop `key` : Comme montré dans notre exemple `CompleteProductForm`, vous pouvez assigner une `key` unique à un input ou au formulaire entier. Lorsque la clé change, React démonte l'ancien composant et en monte un nouveau, réinitialisant ainsi son état. Lier la clé à un indicateur de succès (`key={state.success ? 'success' : 'initial'}`) est une méthode simple et efficace.
- Composants contrôlés : Vous pouvez toujours utiliser des composants contrôlés si nécessaire. En gérant la valeur de l'input avec useState, vous pouvez appeler la fonction de mise à jour pour le vider à l'intérieur d'un useEffect qui écoute l'état de succès de useActionState.
Pièges courants et bonnes pratiques
- Placement de
useFormStatus
: Rappelez-vous, un composant appelant useFormStatus doit être rendu en tant qu'enfant du `<form>`. Il ne fonctionnera pas s'il est un frère ou un parent. - État sérialisable : Lorsque vous utilisez des Server Actions, l'objet d'état retourné par votre action doit être sérialisable. Cela signifie qu'il ne peut pas contenir de fonctions, de symboles ou d'autres valeurs non sérialisables. Tenez-vous-en aux objets simples, tableaux, chaînes de caractères, nombres et booléens.
- Ne pas lancer d'erreurs dans les actions : Au lieu de `throw new Error()`, votre fonction d'action doit gérer les erreurs avec élégance et retourner un objet d'état qui décrit l'erreur (par exemple, `{ success: false, message: 'Une erreur est survenue' }`). Cela garantit que l'état est toujours mis à jour de manière prévisible.
- Définir une forme d'état claire : Établissez une structure cohérente pour votre objet d'état dès le début. Une forme comme `{ data: T | null, message: string | null, success: boolean, errors: Record
| null }` peut couvrir de nombreux cas d'usage.
useActionState vs. useReducer : Une comparaison rapide
À première vue, useActionState peut sembler similaire à useReducer, car tous deux impliquent la mise à jour d'un état basé sur un état précédent. Cependant, ils servent des objectifs distincts.
useReducer
est un hook générique pour gérer des transitions d'état complexes côté client. Il est déclenché par le dispatch d'actions et est idéal pour une logique d'état qui a de nombreux changements d'état possibles et synchrones (par exemple, un assistant complexe à plusieurs étapes).useActionState
est un hook spécialisé conçu pour un état qui change en réponse à une seule action, généralement asynchrone. Son rôle principal est de s'intégrer avec les formulaires HTML, les Server Actions et les fonctionnalités de rendu concurrent de React comme les transitions d'état en attente.
À retenir : Pour les soumissions de formulaires et les opérations asynchrones liées aux formulaires, useActionState est l'outil moderne et spécialement conçu. Pour d'autres machines à états complexes côté client, useReducer reste un excellent choix.
Conclusion : Adopter l'avenir des formulaires React
Le hook useActionState est plus qu'une simple nouvelle API ; il représente un changement fondamental vers une manière plus robuste, déclarative et centrée sur l'utilisateur de gérer les formulaires et les mutations de données dans React. En l'adoptant, vous gagnez :
- Moins de code répétitif : Un seul hook remplace plusieurs appels à useState et l'orchestration manuelle de l'état.
- États d'attente intégrés : Gérez en toute fluidité les interfaces de chargement avec le hook compagnon useFormStatus.
- Amélioration progressive intégrée : Écrivez du code qui fonctionne avec ou sans JavaScript, garantissant l'accessibilité et la résilience pour tous les utilisateurs.
- Communication serveur simplifiée : Une adéquation naturelle avec les Server Actions, rationalisant l'expérience de développement full-stack.
Lorsque vous commencez de nouveaux projets ou que vous refactorisez des projets existants, envisagez d'utiliser useActionState. Non seulement cela améliorera votre expérience de développeur en rendant votre code plus propre et plus prévisible, mais cela vous permettra également de construire des applications de meilleure qualité, plus rapides, plus résilientes et accessibles à un public mondial diversifié.