Back

Optimisation des appels API dans React : Stratégies de debounce expliquées

Optimisation des appels API dans React : Stratégies de debounce expliquées

Lors de la création d’applications React avec des champs de recherche, des fonctionnalités d’autocomplétion, ou toute saisie qui déclenche des appels API, vous rencontrerez rapidement un problème courant : trop de requêtes inutiles. Un utilisateur qui tape “react framework” dans une zone de recherche peut générer 14 appels API distincts en quelques secondes seulement—un pour chaque frappe de touche. Cela gaspille non seulement la bande passante, mais peut surcharger vos serveurs, dégrader les performances, et même entraîner des coûts supplémentaires avec les API facturées par requête.

Le debouncing résout ce problème en retardant l’exécution d’une fonction jusqu’après une pause spécifiée dans les événements. Dans cet article, j’expliquerai comment fonctionne le debouncing dans React, vous montrerai comment l’implémenter correctement, et vous aiderai à éviter les pièges courants qui peuvent casser votre implémentation de debounce.

Points clés à retenir

  • Le debouncing prévient les appels API excessifs en retardant l’exécution jusqu’à ce que les événements de saisie se soient arrêtés
  • Créez des fonctions de debounce en dehors des cycles de rendu en utilisant useCallback ou des hooks personnalisés
  • Passez les valeurs comme arguments plutôt que d’y accéder via des closures
  • Nettoyez les timeouts lorsque les composants se démontent
  • Considérez l’utilisation de bibliothèques établies pour le code de production
  • Ajoutez des états de chargement pour une meilleure expérience utilisateur

Comprendre le problème : Pourquoi le debounce est important

Considérez ce composant de recherche apparemment innocent :

function SearchComponent() {
  const [query, setQuery] = useState('');
  
  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value);
    fetchSearchResults(value); // Appel API à chaque frappe
  };
  
  return (
    <input 
      type="text" 
      value={query}
      onChange={handleSearch}
      placeholder="Search..." 
    />
  );
}

Chaque frappe de touche déclenche un appel API. Pour un utilisateur rapide saisissant “react hooks”, cela représente 11 requêtes API distinctes en succession rapide, avec seulement la dernière affichant les résultats que l’utilisateur veut réellement.

Cela crée plusieurs problèmes :

  1. Gaspillage de ressources : La plupart de ces requêtes sont immédiatement obsolètes
  2. UX médiocre : Résultats qui clignotent lorsque les réponses arrivent dans le désordre
  3. Surcharge serveur : Particulièrement problématique à grande échelle
  4. Limitation de débit : Les API tierces peuvent limiter ou bloquer votre application

Qu’est-ce que le debouncing ?

Le debouncing est une technique de programmation qui garantit qu’une fonction ne s’exécute qu’après qu’un certain laps de temps s’est écoulé depuis sa dernière invocation. Pour les saisies, cela signifie attendre que l’utilisateur ait arrêté de taper avant de faire un appel API.

Voici une représentation visuelle de ce qui se passe :

Sans debouncing :

Frappes :    r → re → rea → reac → react
Appels API : r → re → rea → reac → react (5 appels)

Avec debouncing (300ms) :

Frappes :    r → re → rea → reac → react → [pause 300ms]
Appels API :                                react (1 appel)

Implémentation de base du debounce

Implémentons une fonction de debounce simple :

function debounce(func, delay) {
  let timeoutId;
  
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

Cette fonction :

  1. Prend une fonction et un délai comme paramètres
  2. Retourne une nouvelle fonction qui encapsule l’originale
  3. Efface tout timeout existant lorsqu’elle est appelée
  4. Définit un nouveau timeout pour exécuter la fonction originale après le délai

Implémenter le debounce dans React (La mauvaise façon)

Beaucoup de développeurs essaient d’implémenter le debounce dans React comme ceci :

function SearchComponent() {
  const [query, setQuery] = useState('');
  
  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value);
    
    // Cela semble correct, mais ne l'est pas !
    const debouncedFetch = debounce(() => {
      fetchSearchResults(value);
    }, 300);
    
    debouncedFetch();
  };
  
  return (
    <input 
      type="text" 
      value={query}
      onChange={handleSearch}
      placeholder="Search..." 
    />
  );
}

Le problème : Cela crée une nouvelle fonction de debounce à chaque frappe, annulant complètement l’objectif. Chaque frappe crée son propre timeout isolé qui ne connaît pas les précédents.

Debounce React bien fait

Il y a trois approches principales pour implémenter correctement le debouncing dans React :

1. Utiliser useCallback pour des références de fonction stables

function SearchComponent() {
  const [query, setQuery] = useState('');
  
  // Créer la fonction debounced une seule fois
  const debouncedFetch = useCallback(
    debounce((value) => {
      fetchSearchResults(value);
    }, 300),
    [] // Tableau de dépendances vide signifie que c'est créé une seule fois
  );
  
  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedFetch(value);
  };
  
  return (
    <input 
      type="text" 
      value={query}
      onChange={handleSearch}
      placeholder="Search..." 
    />
  );
}

2. Utiliser un hook personnalisé pour une implémentation plus propre

Créer un hook personnalisé rend votre logique de debounce réutilisable et plus propre :

function useDebounce(callback, delay) {
  const timeoutRef = useRef(null);
  
  useEffect(() => {
    // Nettoyer le timeout lorsque le composant se démonte
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);
  
  const debouncedCallback = useCallback((...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    
    timeoutRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  }, [callback, delay]);
  
  return debouncedCallback;
}

Utilisation du hook :

function SearchComponent() {
  const [query, setQuery] = useState('');
  
  const fetchResults = useCallback((searchTerm) => {
    fetchSearchResults(searchTerm);
  }, []);
  
  const debouncedFetch = useDebounce(fetchResults, 300);
  
  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedFetch(value);
  };
  
  return (
    <input 
      type="text" 
      value={query}
      onChange={handleSearch}
      placeholder="Search..." 
    />
  );
}

3. Utiliser une bibliothèque établie

Pour les applications de production, considérez l’utilisation d’une bibliothèque éprouvée :

import { useDebouncedCallback } from 'use-debounce';

function SearchComponent() {
  const [query, setQuery] = useState('');
  
  const debouncedFetch = useDebouncedCallback(
    (value) => {
      fetchSearchResults(value);
    },
    300
  );
  
  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedFetch(value);
  };
  
  return (
    <input 
      type="text" 
      value={query}
      onChange={handleSearch}
      placeholder="Search..." 
    />
  );
}

Les bibliothèques de debounce populaires pour React incluent :

Pièges courants avec le debounce React

1. Recréer la fonction de debounce au rendu

L’erreur la plus courante est de créer un nouveau wrapper de debounce à chaque rendu :

// ❌ Incorrect - crée une nouvelle fonction de debounce à chaque rendu
const handleChange = (e) => {
  const value = e.target.value;
  debounce(() => fetchData(value), 300)();
};

2. Problèmes de closure avec l’accès au state

Lorsque votre fonction debounced a besoin d’accéder au state le plus récent :

// ❌ Incorrect - capturera les anciennes valeurs de state
const debouncedFetch = useCallback(
  debounce(() => {
    // Ceci utilisera la valeur de query depuis la création de la fonction debounce
    fetchSearchResults(query);
  }, 300),
  [] // Tableau de dépendances vide signifie que ceci capture la valeur initiale de query
);

// ✅ Correct - passer la valeur comme argument
const debouncedFetch = useCallback(
  debounce((value) => {
    fetchSearchResults(value);
  }, 300),
  []
);

3. Ne pas nettoyer les timeouts

Omettre d’effacer les timeouts lorsque les composants se démontent peut causer des fuites mémoire :

// ✅ Correct - nettoyer au démontage
useEffect(() => {
  return () => {
    debouncedFetch.cancel(); // Si utilisation d'une bibliothèque avec méthode cancel
    // ou effacer votre référence de timeout
  };
}, [debouncedFetch]);

Patterns avancés de debounce

Debouncing avec exécution immédiate

Parfois vous voulez exécuter la fonction immédiatement au premier appel, puis debouncer les appels suivants :

function useDebounceWithImmediate(callback, delay, immediate = false) {
  const timeoutRef = useRef(null);
  const isFirstCallRef = useRef(true);
  
  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);
  
  return useCallback((...args) => {
    const callNow = immediate && isFirstCallRef.current;
    
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    
    if (callNow) {
      callback(...args);
      isFirstCallRef.current = false;
    }
    
    timeoutRef.current = setTimeout(() => {
      if (!callNow) callback(...args);
      isFirstCallRef.current = immediate;
    }, delay);
  }, [callback, delay, immediate]);
}

Debouncing avec état de chargement

Pour une meilleure UX, vous pourriez vouloir afficher un indicateur de chargement :

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [results, setResults] = useState([]);
  
  const debouncedFetch = useDebouncedCallback(
    async (value) => {
      try {
        setIsLoading(true);
        const data = await fetchSearchResults(value);
        setResults(data);
      } finally {
        setIsLoading(false);
      }
    },
    300
  );
  
  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value);
    if (value) {
      setIsLoading(true); // Afficher le chargement immédiatement
      debouncedFetch(value);
    } else {
      setResults([]);
      setIsLoading(false);
    }
  };
  
  return (
    <>
      <input 
        type="text" 
        value={query}
        onChange={handleSearch}
        placeholder="Search..." 
      />
      {isLoading && <div>Chargement...</div>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

FAQ

Le debouncing retarde l'exécution jusqu'après une pause dans les événements, idéal pour les champs de recherche où vous voulez la valeur finale. Le throttling limite l'exécution à une fois par intervalle de temps, mieux pour les événements continus comme le défilement ou le redimensionnement.

300-500ms est courant pour les champs de recherche. C'est un équilibre entre réactivité et réduction des appels inutiles. Testez avec de vrais utilisateurs pour trouver la bonne valeur pour votre application.

Oui, votre fonction debounced peut être async. Assurez-vous simplement de gérer les promesses et erreurs correctement en encapsulant votre logique async dans un bloc try-catch et en mettant à jour l'état en conséquence.

Non. Vous devriez debouncer les événements de saisie lorsque l'opération est coûteuse, comme les appels API ou les calculs lourds, lorsque les valeurs intermédiaires ne sont pas nécessaires, ou lorsqu'un léger délai ne nuira pas à l'expérience utilisateur. Cependant, évitez le debouncing lorsque un feedback immédiat est nécessaire, comme pour les indicateurs de validation ou les compteurs de caractères, lorsque l'opération est légère, ou lorsqu'une haute réactivité est importante pour l'expérience utilisateur.

Conclusion

En implémentant un debouncing approprié dans vos applications React, vous créerez une expérience utilisateur plus efficace et réactive tout en réduisant la charge serveur inutile et les coûts potentiels. Que vous choisissiez de créer votre propre implémentation de debounce ou d’utiliser une bibliothèque établie, la clé est de s’assurer que votre fonction de debounce persiste entre les rendus et gère correctement le nettoyage pour prévenir les fuites mémoire.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers