Découvrez le hook useActionState de React pour une gestion d'état simplifiée, déclenchée par des actions asynchrones. Améliorez l'efficacité et l'expérience utilisateur de votre application.
Implémentation de useActionState dans React : Gestion d'état basée sur les actions
Le hook useActionState de React, introduit dans les versions récentes, offre une approche affinée pour gérer les mises à jour d'état résultant d'actions asynchrones. Cet outil puissant simplifie le processus de gestion des mutations, de mise à jour de l'interface utilisateur et de gestion des états d'erreur, en particulier lorsqu'on travaille avec les React Server Components (RSC) et les actions serveur. Ce guide explorera les subtilités de useActionState, en fournissant des exemples pratiques et les meilleures pratiques pour son implémentation.
Comprendre le besoin d'une gestion d'état basée sur les actions
La gestion d'état traditionnelle dans React implique souvent de gérer les états de chargement et d'erreur séparément au sein des composants. Lorsqu'une action (par exemple, soumettre un formulaire, récupérer des données) déclenche une mise à jour d'état, les développeurs gèrent généralement ces états avec plusieurs appels à useState et une logique conditionnelle potentiellement complexe. useActionState fournit une solution plus propre et plus intégrée.
Considérons un scénario simple de soumission de formulaire. Sans useActionState, vous pourriez avoir :
- Une variable d'état pour les données du formulaire.
- Une variable d'état pour suivre si le formulaire est en cours de soumission (état de chargement).
- Une variable d'état pour contenir les messages d'erreur.
Cette approche peut conduire à du code verbeux et à des incohérences potentielles. useActionState consolide ces préoccupations en un seul hook, simplifiant la logique et améliorant la lisibilité du code.
Présentation de useActionState
Le hook useActionState accepte deux arguments :
- Une fonction asynchrone (l'« action ») qui effectue la mise à jour de l'état. Il peut s'agir d'une action serveur ou de toute autre fonction asynchrone.
- Une valeur d'état initiale.
Il retourne un tableau contenant deux éléments :
- La valeur actuelle de l'état.
- Une fonction pour dispatcher l'action. Cette fonction gère automatiquement les états de chargement et d'erreur associés à l'action.
Voici un exemple de base :
import { useActionState } from 'react';
async function updateServer(prevState, formData) {
// Simule une mise à jour asynchrone du serveur.
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
return 'Échec de la mise à jour du serveur.';
}
return `Nom mis à jour vers : ${data.name}`;
}
function MyComponent() {
const [state, dispatch] = useActionState(updateServer, 'État Initial');
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const result = await dispatch(formData);
console.log(result);
}
return (
);
}
Dans cet exemple :
updateServerest l'action asynchrone qui simule la mise à jour d'un serveur. Elle reçoit l'état précédent et les données du formulaire.useActionStateinitialise l'état avec 'État Initial' et retourne l'état actuel ainsi que la fonctiondispatch.- La fonction
handleSubmitappelledispatchavec les données du formulaire.useActionStategère automatiquement les états de chargement et d'erreur pendant l'exécution de l'action.
Gestion des états de chargement et d'erreur
L'un des avantages clés de useActionState est sa gestion intégrée des états de chargement et d'erreur. La fonction dispatch retourne une promesse qui se résout avec le résultat de l'action. Si l'action lève une erreur, la promesse est rejetée avec l'erreur. Vous pouvez utiliser cela pour mettre à jour l'interface utilisateur en conséquence.
Modifions l'exemple précédent pour afficher un message de chargement et un message d'erreur :
import { useActionState } from 'react';
import { useState } from 'react';
async function updateServer(prevState, formData) {
// Simule une mise à jour asynchrone du serveur.
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
throw new Error('Échec de la mise à jour du serveur.');
}
return `Nom mis à jour vers : ${data.name}`;
}
function MyComponent() {
const [state, dispatch] = useActionState(updateServer, 'État Initial');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
setIsSubmitting(true);
setErrorMessage(null);
try {
const result = await dispatch(formData);
console.log(result);
} catch (error) {
console.error("Erreur lors de la soumission :", error);
setErrorMessage(error.message);
} finally {
setIsSubmitting(false);
}
}
return (
);
}
Changements clés :
- Nous avons ajouté les variables d'état
isSubmittingeterrorMessagepour suivre les états de chargement et d'erreur. - Dans
handleSubmit, nous mettonsisSubmittingàtrueavant d'appelerdispatchet nous attrapons les erreurs pour mettre à jourerrorMessage. - Nous désactivons le bouton de soumission pendant le chargement et affichons les messages de chargement et d'erreur de manière conditionnelle.
useActionState avec les Actions Serveur dans les React Server Components (RSC)
useActionState brille lorsqu'il est utilisé avec les React Server Components (RSC) et les actions serveur. Les actions serveur sont des fonctions qui s'exécutent sur le serveur et peuvent directement muter les sources de données. Elles vous permettent d'effectuer des opérations côté serveur sans écrire de points de terminaison d'API.
Note : Cet exemple nécessite un environnement React configuré pour les Server Components et les Server Actions.
// app/actions.js (Action Serveur)
'use server';
import { cookies } from 'next/headers'; //Exemple, pour Next.js
export async function updateName(prevState, formData) {
const name = formData.get('name');
if (!name) {
return 'Veuillez saisir un nom.';
}
try {
// Simule une mise à jour de la base de données.
await new Promise(resolve => setTimeout(resolve, 1000));
cookies().set('userName', name);
return `Nom mis à jour vers : ${name}`; //Succès !
} catch (error) {
console.error("Échec de la mise à jour de la base de données :", error);
return 'Échec de la mise à jour du nom.'; // Important : Retourner un message, ne pas lever une Erreur
}
}
// app/page.jsx (React Server Component)
'use client';
import { useActionState } from 'react';
import { updateName } from './actions';
function MyComponent() {
const [state, dispatch] = useActionState(updateName, 'État Initial');
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const result = await dispatch(formData);
console.log(result);
}
return (
);
}
export default MyComponent;
Dans cet exemple :
updateNameest une action serveur définie dansapp/actions.js. Elle reçoit l'état précédent et les données du formulaire, met à jour la base de données (simulée) et retourne un message de succès ou d'erreur. Point crucial, l'action retourne un message plutôt que de lever une erreur. Les Actions Serveur préfèrent retourner des messages informatifs.- Le composant est marqué comme un composant client (
'use client') pour utiliser le hookuseActionState. - La fonction
handleSubmitappelledispatchavec les données du formulaire.useActionStategère automatiquement la mise à jour de l'état en fonction du résultat de l'action serveur.
Considérations importantes pour les Actions Serveur
- Gestion des erreurs dans les Actions Serveur : Au lieu de lever des erreurs, retournez un message d'erreur significatif depuis votre Action Serveur.
useActionStatetraitera ce message comme le nouvel état. Cela permet une gestion des erreurs élégante côté client. - Mises à jour optimistes : Les actions serveur peuvent être utilisées avec des mises à jour optimistes pour améliorer la performance perçue. Vous pouvez mettre à jour l'interface utilisateur immédiatement et revenir en arrière si l'action échoue.
- Revalidation : Après une mutation réussie, envisagez de revalider les données mises en cache pour vous assurer que l'interface utilisateur reflète le dernier état.
Techniques avancées avec useActionState
1. Utiliser un réducteur pour les mises à jour d'état complexes
Pour une logique d'état plus complexe, vous pouvez combiner useActionState avec une fonction réducteur. Cela vous permet de gérer les mises à jour d'état de manière prévisible et maintenable.
import { useActionState } from 'react';
import { useReducer } from 'react';
const initialState = {
count: 0,
message: 'État Initial',
};
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_MESSAGE':
return { ...state, message: action.payload };
default:
return state;
}
}
async function updateState(state, action) {
// Simule une opération asynchrone.
await new Promise(resolve => setTimeout(resolve, 500));
switch (action.type) {
case 'INCREMENT':
return reducer(state, action);
case 'DECREMENT':
return reducer(state, action);
case 'SET_MESSAGE':
return reducer(state, action);
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useActionState(updateState, initialState);
return (
Compteur : {state.count}
Message : {state.message}
);
}
2. Mises à jour optimistes avec useActionState
Les mises à jour optimistes améliorent l'expérience utilisateur en mettant immédiatement à jour l'interface utilisateur comme si l'action avait réussi, puis en annulant la mise à jour si l'action échoue. Cela peut donner à votre application une sensation de plus grande réactivité.
import { useActionState } from 'react';
import { useState } from 'react';
async function updateServer(prevState, formData) {
// Simule une mise à jour asynchrone du serveur.
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
throw new Error('Échec de la mise à jour du serveur.');
}
return `Nom mis à jour vers : ${data.name}`;
}
function MyComponent() {
const [name, setName] = useState('Nom Initial');
const [state, dispatch] = useActionState(async (prevName, newName) => {
try {
const result = await updateServer(prevName, {
name: newName,
});
return newName; // Mettre à jour en cas de succès
} catch (error) {
// Annuler en cas d'erreur
console.error("La mise à jour a échoué :", error);
setName(prevName);
return prevName;
}
}, name);
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const newName = formData.get('name');
setName(newName); // Mettre à jour l'UI de manière optimiste
await dispatch(newName);
}
return (
);
}
3. Débounce des actions
Dans certains scénarios, vous pourriez vouloir délimiter (debounce) les actions pour éviter qu'elles ne soient dispatchées trop fréquemment. Cela peut être utile pour des scénarios comme les champs de recherche où vous ne voulez déclencher une action qu'une fois que l'utilisateur a cessé de taper pendant une certaine période.
import { useActionState } from 'react';
import { useState, useEffect } from 'react';
async function searchItems(prevState, query) {
// Simule une recherche asynchrone.
await new Promise(resolve => setTimeout(resolve, 500));
return `Résultats de recherche pour : ${query}`;
}
function MyComponent() {
const [query, setQuery] = useState('');
const [state, dispatch] = useActionState(searchItems, 'État Initial');
useEffect(() => {
const timeoutId = setTimeout(() => {
if (query) {
dispatch(query);
}
}, 300); // Débounce de 300ms
return () => clearTimeout(timeoutId);
}, [query, dispatch]);
return (
setQuery(e.target.value)}
/>
État : {state}
);
}
Meilleures pratiques pour useActionState
- Gardez les actions pures : Assurez-vous que vos actions sont des fonctions pures (ou aussi proches que possible). Elles ne devraient pas avoir d'effets de bord autres que la mise à jour de l'état.
- Gérez les erreurs avec élégance : Gérez toujours les erreurs dans vos actions et fournissez des messages d'erreur informatifs à l'utilisateur. Comme indiqué ci-dessus avec les Actions Serveur, privilégiez le retour d'une chaîne de message d'erreur depuis l'action serveur plutôt que de lever une erreur.
- Optimisez les performances : Soyez conscient des implications de performance de vos actions, en particulier lorsque vous traitez de grands ensembles de données. Envisagez d'utiliser des techniques de mémoïsation pour éviter les re-rendus inutiles.
- Considérez l'accessibilité : Assurez-vous que votre application reste accessible à tous les utilisateurs, y compris ceux en situation de handicap. Fournissez les attributs ARIA et la navigation au clavier appropriés.
- Tests approfondis : Rédigez des tests unitaires et des tests d'intégration pour vous assurer que vos actions et vos mises à jour d'état fonctionnent correctement.
- Internationalisation (i18n) : Pour les applications mondiales, implémentez l'i18n pour prendre en charge plusieurs langues et cultures.
- Localisation (l10n) : Adaptez votre application à des localités spécifiques en fournissant du contenu, des formats de date et des symboles monétaires localisés.
useActionState vs. autres solutions de gestion d'état
Bien que useActionState offre un moyen pratique de gérer les mises à jour d'état basées sur des actions, il ne remplace pas toutes les solutions de gestion d'état. Pour les applications complexes avec un état global qui doit être partagé entre plusieurs composants, des bibliothèques comme Redux, Zustand ou Jotai pourraient être plus appropriées.
Quand utiliser useActionState :
- Mises à jour d'état de complexité simple à modérée.
- Mises à jour d'état étroitement liées à des actions asynchrones.
- Intégration avec les React Server Components et les Actions Serveur.
Quand envisager d'autres solutions :
- Gestion d'état global complexe.
- État qui doit être partagé entre un grand nombre de composants.
- Fonctionnalités avancées comme le débogage temporel (time-travel debugging) ou les middlewares.
Conclusion
Le hook useActionState de React offre un moyen puissant et élégant de gérer les mises à jour d'état déclenchées par des actions asynchrones. En consolidant les états de chargement et d'erreur, il simplifie le code et améliore la lisibilité, en particulier lorsqu'on travaille avec les React Server Components et les actions serveur. Comprendre ses forces et ses limites vous permet de choisir la bonne approche de gestion d'état pour votre application, conduisant à un code plus maintenable et efficace.
En suivant les meilleures pratiques décrites dans ce guide, vous pouvez tirer parti efficacement de useActionState pour améliorer l'expérience utilisateur et le flux de travail de développement de votre application. N'oubliez pas de tenir compte de la complexité de votre application et de choisir la solution de gestion d'état qui correspond le mieux à vos besoins. Des simples soumissions de formulaires aux mutations de données complexes, useActionState peut être un outil précieux dans votre arsenal de développement React.