Découvrez comment utiliser le hook useActionState de React pour implémenter le debouncing, limiter le débit des actions et optimiser les performances de vos applications.
React useActionState : Implémenter le Debouncing pour une limitation optimale du débit des actions
Dans les applications web modernes, la gestion efficace des interactions utilisateur est primordiale. Des actions telles que les soumissions de formulaires, les requêtes de recherche et les mises à jour de données déclenchent souvent des opérations côté serveur. Cependant, des appels excessifs au serveur, surtout s'ils sont déclenchés en succession rapide, peuvent entraîner des goulots d'étranglement de performance et une expérience utilisateur dégradée. C'est là que le debouncing entre en jeu, et le hook useActionState de React offre une solution puissante et élégante.
Qu'est-ce que le Debouncing ?
Le debouncing est une pratique de programmation utilisée pour s'assurer que les tâches gourmandes en temps ne se déclenchent pas trop souvent, en retardant l'exécution d'une fonction jusqu'à ce qu'une certaine période d'inactivité soit passée. Imaginez que vous recherchez un produit sur un site de commerce électronique. Sans debouncing, chaque frappe dans la barre de recherche déclencherait une nouvelle requête au serveur pour récupérer les résultats. Cela pourrait surcharger le serveur et offrir une expérience saccadée et peu réactive à l'utilisateur. Avec le debouncing, la requête de recherche n'est envoyée qu'après que l'utilisateur a cessé de taper pendant une courte période (par exemple, 300 millisecondes).
Pourquoi utiliser useActionState pour le Debouncing ?
useActionState, introduit dans React 18, fournit un mécanisme pour gérer les mises à jour d'état asynchrones résultant d'actions, en particulier au sein des React Server Components. Il est particulièrement utile avec les actions serveur car il permet de gérer les états de chargement et les erreurs directement dans votre composant. Associé aux techniques de debouncing, useActionState offre un moyen propre et performant de gérer les interactions avec le serveur déclenchées par les saisies de l'utilisateur. Avant useActionState, l'implémentation de ce type de fonctionnalité impliquait souvent une gestion manuelle de l'état avec useState et useEffect, ce qui conduisait à un code plus verbeux et potentiellement sujet aux erreurs.
Implémenter le Debouncing avec useActionState : Guide étape par étape
Explorons un exemple pratique d'implémentation du debouncing avec useActionState. Nous allons considérer un scénario où un utilisateur tape dans un champ de saisie, et nous voulons mettre à jour une base de données côté serveur avec le texte saisi, mais seulement après un court délai.
Étape 1 : Mettre en place le composant de base
D'abord, nous allons créer un composant fonctionnel simple avec un champ de saisie :
import React, { useState, useCallback } from 'react';
import { useActionState } from 'react-dom/server';
async function updateDatabase(prevState: any, formData: FormData) {
// Simule une mise à jour de la base de données
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simule la latence du réseau
return { success: true, message: `Mis Ă jour avec : ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const handleChange = (event: React.ChangeEvent) => {
const newText = event.target.value;
setDebouncedText(newText);
};
return (
<form action={dispatch}>
<input type="text" name="text" value={debouncedText} onChange={handleChange} />
<button type="submit">Mettre Ă jour</button>
<p>{state.message}</p>
</form>
);
}
export default MyComponent;
Dans ce code :
- Nous importons les hooks nécessaires :
useState,useCallback, etuseActionState. - Nous définissons une fonction asynchrone
updateDatabasequi simule une mise à jour côté serveur. Cette fonction prend l'état précédent et les données du formulaire comme arguments. useActionStateest initialisé avec la fonctionupdateDatabaseet un objet d'état initial.- La fonction
handleChangemet à jour l'état localdebouncedTextavec la valeur de l'input.
Étape 2 : Implémenter la logique de Debounce
Maintenant, nous allons introduire la logique de debouncing. Nous utiliserons les fonctions setTimeout et clearTimeout pour retarder l'appel à la fonction dispatch retournée par useActionState.
import React, { useState, useRef, useCallback } from 'react';
import { useActionState } from 'react-dom/server';
async function updateDatabase(prevState: any, formData: FormData) {
// Simule une mise à jour de la base de données
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simule la latence du réseau
return { success: true, message: `Mis Ă jour avec : ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const timeoutRef = useRef(null);
const handleChange = (event: React.ChangeEvent) => {
const newText = event.target.value;
setDebouncedText(newText);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
const formData = new FormData();
formData.append('text', newText);
dispatch(formData);
}, 300);
};
return (
<div>
<input type="text" value={debouncedText} onChange={handleChange} />
<p>{state.message}</p>
</div>
);
}
export default MyComponent;
Voici ce qui a changé :
- Nous avons ajouté un hook
useRefappelétimeoutRefpour stocker l'ID du timeout. Cela nous permet d'annuler le timeout si l'utilisateur tape à nouveau avant que le délai ne soit écoulé. - À l'intérieur de
handleChange: - Nous annulons tout timeout existant en utilisant
clearTimeoutsitimeoutRef.currenta une valeur. - Nous définissons un nouveau timeout avec
setTimeout. Ce timeout exécutera la fonctiondispatch(avec les données de formulaire mises à jour) après 300 millisecondes d'inactivité. - Nous avons déplacé l'appel de dispatch hors du formulaire et dans la fonction "debounced". Nous utilisons maintenant un élément input standard plutôt qu'un formulaire, et nous déclenchons l'action serveur de manière programmatique.
Étape 3 : Optimisation pour les performances et les fuites de mémoire
L'implémentation précédente est fonctionnelle, mais elle peut être encore optimisée pour éviter les fuites de mémoire potentielles. Si le composant est démonté alors qu'un timeout est toujours en attente, le rappel du timeout s'exécutera quand même, ce qui pourrait entraîner des erreurs ou un comportement inattendu. Nous pouvons éviter cela en annulant le timeout dans le hook useEffect lorsque le composant est démonté :
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { useActionState } from 'react-dom/server';
async function updateDatabase(prevState: any, formData: FormData) {
// Simule une mise à jour de la base de données
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simule la latence du réseau
return { success: true, message: `Mis Ă jour avec : ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const timeoutRef = useRef(null);
const handleChange = (event: React.ChangeEvent) => {
const newText = event.target.value;
setDebouncedText(newText);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
const formData = new FormData();
formData.append('text', newText);
dispatch(formData);
}, 300);
};
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return (
<div>
<input type="text" value={debouncedText} onChange={handleChange} />
<p>{state.message}</p>
</div>
);
}
export default MyComponent;
Nous avons ajouté un hook useEffect avec un tableau de dépendances vide. Cela garantit que l'effet ne s'exécute que lorsque le composant est monté et démonté. À l'intérieur de la fonction de nettoyage de l'effet (retournée par l'effet), nous annulons le timeout s'il existe. Cela empêche le rappel du timeout de s'exécuter après que le composant a été démonté.
Alternative : Utiliser une bibliothèque de Debounce
Bien que l'implémentation ci-dessus démontre les concepts de base du debouncing, l'utilisation d'une bibliothèque dédiée peut simplifier le code et réduire le risque d'erreurs. Des bibliothèques comme lodash.debounce fournissent des implémentations de debouncing robustes et bien testées.
Voici comment vous pouvez utiliser lodash.debounce avec useActionState :
import React, { useState, useCallback, useEffect } from 'react';
import { useActionState } from 'react-dom/server';
import debounce from 'lodash.debounce';
async function updateDatabase(prevState: any, formData: FormData) {
// Simule une mise à jour de la base de données
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simule la latence du réseau
return { success: true, message: `Mis Ă jour avec : ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const debouncedDispatch = useCallback(debounce((text: string) => {
const formData = new FormData();
formData.append('text', text);
dispatch(formData);
}, 300), [dispatch]);
const handleChange = (event: React.ChangeEvent) => {
const newText = event.target.value;
setDebouncedText(newText);
debouncedDispatch(newText);
};
return (
<div>
<input type="text" value={debouncedText} onChange={handleChange} />
<p>{state.message}</p>
</div>
);
}
export default MyComponent;
Dans cet exemple :
- Nous importons la fonction
debouncedelodash.debounce. - Nous créons une version "debounced" de la fonction
dispatchen utilisantuseCallbacketdebounce. Le hookuseCallbackgarantit que la fonction "debounced" n'est créée qu'une seule fois, et le tableau de dépendances inclutdispatchpour s'assurer que la fonction est mise à jour si la fonctiondispatchchange. - Dans la fonction
handleChange, nous appelons simplement la fonctiondebouncedDispatchavec le nouveau texte.
Considérations globales et meilleures pratiques
Lors de l'implémentation du debouncing, en particulier dans les applications destinées à un public mondial, tenez compte des points suivants :
- Latence réseau : La latence réseau peut varier considérablement en fonction de l'emplacement de l'utilisateur et des conditions du réseau. Un délai de debounce qui fonctionne bien pour les utilisateurs d'une région peut être trop court ou trop long pour ceux d'une autre. Envisagez de permettre aux utilisateurs de personnaliser le délai de debounce ou de l'ajuster dynamiquement en fonction des conditions du réseau. Ceci est particulièrement important pour les applications utilisées dans des régions où l'accès à Internet est peu fiable, comme certaines parties de l'Afrique ou de l'Asie du Sud-Est.
- Éditeurs de méthode d'entrée (IME) : Les utilisateurs de nombreux pays asiatiques utilisent des IME pour saisir du texte. Ces éditeurs nécessitent souvent plusieurs frappes pour composer un seul caractère. Si le délai de debounce est trop court, il peut interférer avec le processus de l'IME, ce qui entraîne une expérience utilisateur frustrante. Envisagez d'augmenter le délai de debounce pour les utilisateurs qui utilisent des IME, ou utilisez un écouteur d'événements plus adapté à la composition IME.
- Accessibilité : Le debouncing peut potentiellement avoir un impact sur l'accessibilité, en particulier pour les utilisateurs ayant des troubles moteurs. Assurez-vous que le délai de debounce n'est pas trop long et fournissez des moyens alternatifs pour que les utilisateurs déclenchent l'action si nécessaire. Par exemple, vous pourriez fournir un bouton de soumission sur lequel les utilisateurs peuvent cliquer pour déclencher manuellement l'action.
- Charge du serveur : Le debouncing aide à réduire la charge du serveur, mais il est toujours important d'optimiser le code côté serveur pour traiter les requêtes efficacement. Utilisez la mise en cache, l'indexation de base de données et d'autres techniques d'optimisation des performances pour minimiser la charge sur le serveur.
- Gestion des erreurs : Implémentez une gestion des erreurs robuste pour gérer avec élégance toute erreur survenant pendant le processus de mise à jour côté serveur. Affichez des messages d'erreur informatifs à l'utilisateur et proposez des options pour réessayer l'action.
- Retour utilisateur : Fournissez un retour visuel clair à l'utilisateur pour indiquer que sa saisie est en cours de traitement. Cela pourrait inclure un spinner de chargement, une barre de progression ou un simple message comme "Mise à jour...". Sans retour clair, les utilisateurs peuvent être déroutés ou frustrés, surtout si le délai de debounce est relativement long.
- Localisation : Assurez-vous que tous les textes et messages sont correctement localisés pour les différentes langues et régions. Cela inclut les messages d'erreur, les indicateurs de chargement et tout autre texte affiché à l'utilisateur.
Exemple : Debouncing d'une barre de recherche
Considérons un exemple plus concret : une barre de recherche dans une application de commerce électronique. Nous voulons appliquer un debounce à la requête de recherche pour éviter d'envoyer trop de requêtes au serveur pendant que l'utilisateur tape.
import React, { useState, useCallback, useEffect } from 'react';
import { useActionState } from 'react-dom/server';
import debounce from 'lodash.debounce';
async function searchProducts(prevState: any, formData: FormData) {
// Simule une recherche de produits
const query = formData.get('query') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simule la latence du réseau
// Dans une application réelle, vous récupéreriez les résultats de recherche d'une base de données ou d'une API ici
const results = [`Produit A correspondant Ă "${query}"`, `Produit B correspondant Ă "${query}"`];
return { success: true, message: `Résultats de recherche pour : ${query}`, results: results };
}
function SearchBar() {
const [searchQuery, setSearchQuery] = useState('');
const [state, dispatch] = useActionState(searchProducts, {success: false, message: "", results: []});
const [searchResults, setSearchResults] = useState([]);
const debouncedSearch = useCallback(debounce((query: string) => {
const formData = new FormData();
formData.append('query', query);
dispatch(formData);
}, 300), [dispatch]);
const handleChange = (event: React.ChangeEvent) => {
const newQuery = event.target.value;
setSearchQuery(newQuery);
debouncedSearch(newQuery);
};
useEffect(() => {
if(state.success){
setSearchResults(state.results);
}
}, [state]);
return (
<div>
<input type="text" placeholder="Rechercher des produits..." value={searchQuery} onChange={handleChange} />
<p>{state.message}</p>
<ul>
{searchResults.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
</div>
);
}
export default SearchBar;
Cet exemple montre comment appliquer un debounce à une requête de recherche en utilisant lodash.debounce et useActionState. La fonction searchProducts simule une recherche de produits, et le composant SearchBar affiche les résultats. Dans une application réelle, la fonction searchProducts récupérerait les résultats d'une API backend.
Au-delà du Debouncing de base : Techniques avancées
Bien que les exemples ci-dessus démontrent le debouncing de base, il existe des techniques plus avancées qui peuvent être utilisées pour optimiser davantage les performances et l'expérience utilisateur :
- Debouncing "Leading Edge" (front montant) : Avec le debouncing standard, la fonction est exécutée après le délai. Avec le debouncing "leading edge", la fonction est exécutée au début du délai, et les appels ultérieurs pendant le délai sont ignorés. Cela peut être utile pour les scénarios où vous souhaitez fournir un retour immédiat à l'utilisateur.
- Debouncing "Trailing Edge" (front descendant) : C'est la technique de debouncing standard, où la fonction est exécutée après le délai.
- Throttling : Le throttling est similaire au debouncing, mais au lieu de retarder l'exécution de la fonction jusqu'à une période d'inactivité, le throttling limite la fréquence à laquelle la fonction peut être appelée. Par exemple, vous pourriez limiter une fonction pour qu'elle soit appelée au maximum une fois toutes les 100 millisecondes.
- Debouncing adaptatif : Le debouncing adaptatif ajuste dynamiquement le délai de debounce en fonction du comportement de l'utilisateur ou des conditions du réseau. Par exemple, vous pourriez diminuer le délai si l'utilisateur tape très lentement, ou l'augmenter si la latence du réseau est élevée.
Conclusion
Le debouncing est une technique cruciale pour optimiser les performances et l'expérience utilisateur des applications web interactives. Le hook useActionState de React offre un moyen puissant et élégant de l'implémenter, en particulier en conjonction avec les React Server Components et les actions serveur. En comprenant les principes du debouncing et les capacités de useActionState, les développeurs peuvent créer des applications réactives, efficaces et conviviales qui s'adaptent à une échelle mondiale. N'oubliez pas de prendre en compte des facteurs tels que la latence du réseau, l'utilisation des IME et l'accessibilité lors de l'implémentation du debouncing dans des applications destinées à un public international. Choisissez la bonne technique de debouncing (leading edge, trailing edge ou adaptative) en fonction des exigences spécifiques de votre application. Tirez parti de bibliothèques comme lodash.debounce pour simplifier l'implémentation et réduire le risque d'erreurs. En suivant ces directives, vous pouvez garantir que vos applications offrent une expérience fluide et agréable aux utilisateurs du monde entier.