Comment faire un input avec un debounce en React

Comment faire un input avec un debounce en React ?

  React Typescript

Introduction

Dans cet article nous allons découvrir comment réaliser un champ input qui comporte un délai, après chaque saisi de caractères.

Le fait d’implémenter ce comportement est intéressant dans les cas où vous avez un nombre de résultats important à filtrer ou trier. Cela permet de rendre votre application plus réactive.

Nous allons voir ensemble comment déclarer correctement un hook React qui va nous permettre d’effectuer un debounce lors de notre saisi.

Notre composant pour filtrer une liste

Nous allons créer un composant FilterList qui accepte une grande liste de noms (au moins 200 enregistrements).

Le composant a un champ de saisie dans lequel l’utilisateur tape une requête, et les noms sont filtrés par la requête.

Voici la première version du composant FilterList non optimisé :

import { useState, FC } from "react";

interface FilterListProps {
  names: string[];
}

export const FilterList: FC<FilterListProps> = ({ names }) => {
  const [query, setQuery] = useState("");

  let filteredNames = names;

  if (query !== "") {
    filteredNames = names.filter((name) => {
      return name.toLowerCase().includes(query.toLowerCase());
    });
  }

  const onChangeHandler = (event: React.FormEvent<HTMLInputElement>) => {
    setQuery(event.target.value);
  };

  return (
    <div>
      <input
        onChange={onChangeHandler}
        type="text"
        placeholder="Search a name..."
      />
      {filteredNames.map((name) => (
        <div key={name}>{name}</div>
      ))}
    </div>
  );
};

Quand vous renseignez le champ de saisie, vous verrez la liste filtrée pour chaque caractère saisi.

Par exemple, si vous tapez caractère par caractère le mot Thibaud, alors le composant affichera les listes filtrées pour les requêtes T, Th, Thi, Thib, Thiba, Thibau, Thibaud. Cependant, l’utilisateur ne souhaite généralement voir qu’un seul résultat de filtre : pour le mot Thibaud.

Adoucissons le filtrage en appliquant un debounce de 300 ms sur la fonction onChangeHandler.

Mise en place du debounce

Pour mettre un délai pour notre fonction changeHandler, je vais utiliser la librairie lodash.debounce.

Voyons d’abord comment utiliser la fonction debounce() :

import debounce from "lodash.debounce";

const debouncedCallback = debounce(callback, waitTime);

La fonction debounce() accepte un callback en premier argument, et renvoie une nouvelle version de cette fonction avec le debounce intégré.

Lorsque la fonction debouncedCallback est invoquée plusieurs fois, en rafales, elle n’invoquera le callback qu’après la durée en milliseconde précisée par waitTime.

Il est important de noter qu’à l’intérieur du composant React la fonction devra garder la même référence entre les nouveaux rendus du composant, afin que cela fonctionne correctement.

Pour ce faire nous allons utiliser le hook useCallback pour conserver la même référence de la fonction entre les rendus de composants.

import { useState, useCallback, FC } from "react";
import debounce from "lodash.debounce";

interface FilterListProps {
  names: string[];
}

export const FilterList: FC<FilterListProps> = ({ names }) => {
  const [query, setQuery] = useState("");

  let filteredNames = names;

  if (query !== "") {
    filteredNames = names.filter((name) => {
      return name.toLowerCase().includes(query.toLowerCase());
    });
  }

  const onChangeHandler = (event: React.FormEvent<HTMLInputElement>) => {

    setQuery(event.target.value); 
  }; 

  const debouncedChangeHandler = useCallback(
    debounce(onChangeHandler, 300),
    []
  );

  return (
    <div>
      <input
        onChange={debouncedChangeHandler}
        type="text"
        placeholder="Search a name..."
      />
      {filteredNames.map((name) => (
        <div key={name}>{name}</div>
      ))}
    </div>
  );
};

Optimisation et nettoyage

Étant donné que le debounce s’effectue avec un temps de latence, il est possible de se retrouver dans une situation où la fonction est exécutée après la destruction du composant React.

La méthode debounce() de lodash fournit une méthode debouncedCallback.cancel() qui permet d’annuler les différentes invocations.

Voici comment vous pouvez annuler la fonction debounce lorsque le composant que le composant React sera détruit :

import { useState, useCallback, useEffect } from 'react';
import debounce from 'lodash.debounce';

export const FilterList: FC<FilterListProps> = ({ names }) => {
  // ....

  const debouncedChangeHandler = useCallback(debounce(onChangeHandler, 300), []);

  // Stop the invocation of the debounced function after unmounting
  useEffect(() => { 
    return () => { 
      debouncedChangeHandler.cancel(); 
    } 
  }, []); 

  return (
    // ....
  );
}

Il est également possible d’améliorer le code ci-dessus en utilisant le hook useMemo à la place de useCallback.

useMemo permettra de garder en mémoire la fonction qui aura été débouncée, et ne s’exécutera qu’une fois après le rendu initial du composant.

import { useState, useMemo, useEffect } from 'react';
import debounce from 'lodash.debounce';

export const FilterList: FC<FilterListProps> = ({ names }) => {
  // ....

  const debouncedChangeHandler = useMemo(debounce(onChangeHandler, 300), []); 

  return (
    // ....
  );
}