Trying to implement debounce in React Project

I’m trying to implement debounce in a small/test React application.

It’s just an application that fetch data from an API and it has a text field for an auto complete.

import React, { useEffect, useState, useMemo } from 'react';
import axios from 'axios';

const API = 'https://jsonplaceholder.typicode.com/posts';

const AutoComplete2 = () => {

    const [ text, setText ] = useState("")
    const [ posts, setPosts ] = useState([])

    useEffect(() => {  
        async function fetchData() {
            const data = await axios.get(API);
            if(parseInt(data.status) !== 200)   return;

            setPosts(data.data)
        }
        fetchData();
    }, [])

    const handleTextChange = (event) => setText(event.target.value);

    const handleSelectOption = (str) => setText(str);

    const showOptions = useMemo(() => {
        if(text === '') return;

        const showPosts = [...posts].filter((ele) => ele.title.toLowerCase().includes(text.toLowerCase()));
        if(showPosts.length === 1) {
            setText(showPosts[0].title);
        } else {
            return (
                <div>
                    {showPosts.map((obj, index) => {
                        return (
                            <div key={index} >
                                <span onClick={() => handleSelectOption(obj.title)} style={{cursor: 'pointer'}}>
                                    {obj.title}
                                </span>
                            </div>
                        )
                    })}
                </div>
            )
        }
    }, [text, posts])

    // addding debounce
    const debounce = (fn, delay) => {
        let timer;
        return function() {
            let context = this;
            let args = arguments;
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn.apply(context, args)
            }, delay);
        }
    }

    const newHandleTextChange = ((val) => debounce(handleTextChange(val), 5000));

    return (
        <div>
            <input type="text" value={text} onChange={newHandleTextChange} />
            {showOptions}
        </div>
    )
}

export default AutoComplete2;

The application works, but not the debounce. I add a 5 seconds wait to clearly see if it is working, but every time I change the input text, it calls the function without the delay. Does anyone know why it is happening?

Thanks

Answer

A more idiomatic approach to debouncing in React is to use a useEffect hook and store the debounced text as a different stateful variable. You can then run your filter on whatever that variable is.

import React, { useEffect, useState, useMemo } from "react";
import axios from "axios";

const API = "https://jsonplaceholder.typicode.com/posts";

const AutoComplete2 = () => {
  const [text, setText] = useState("");
  const [debouncedText, setDebouncedText] = useState("");
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const data = await axios.get(API);
      if (parseInt(data.status) !== 200) return;

      setPosts(data.data);
    }
    fetchData();
  }, []);

  // This will do the debouncing
  // "text" will always be current
  // "debouncedText" will be debounced
  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedText(text);
    }, 5000);
    // Cleanup function clears timeout
    return () => {
      clearTimeout(timeout);
    };
  }, [text]);

  const handleTextChange = (event) => setText(event.target.value);

  const handleSelectOption = (str) => setText(str);

  const showOptions = useMemo(() => {
    if (debouncedText === "") return;

    const showPosts = [...posts].filter((ele) =>
      ele.title.toLowerCase().includes(debouncedText.toLowerCase())
    );
    if (showPosts.length === 1) {
      setText(showPosts[0].title);
    } else {
      return (
        <div>
          {showPosts.map((obj, index) => {
            return (
              <div key={index}>
                <span
                  onClick={() => handleSelectOption(obj.title)}
                  style={{ cursor: "pointer" }}
                >
                  {obj.title}
                </span>
              </div>
            );
          })}
        </div>
      );
    }
  }, [debouncedText, posts]);

  return (
    <div>
      <input type="text" value={text} onChange={handleTextChange} />
      {showOptions}
    </div>
  );
};

export default AutoComplete2;