Search code examples
reactjslodashonchangedebouncing

Why is my React debounce handler never called?


I'm using React 16.13.0 and lodash. I was recommended to use debounce as a way to properly send search requests to my server as a user types in their search term. I implemented this ...

...

const handleChange = (event, searchTerm, setSearchTerm, setSearchResults) => {
  console.log("search term:" + searchTerm);
  const query = event.target.value;
  setSearchTerm(query);
  if (!query) {
    setSearchResults( [] );
  } else {
    doSearch(query, searchTerm, setSearchResults);
  }
}

const getDebouncedHandler = (e, handler, delay) => {
  console.log("value:" + e.target.value);
  _.debounce(handler, delay);
}
...

const Search = (props) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [searchResults, setSearchResults] = useState([]);

  const renderSearchResults = ...

  return (
    <div className="searchForm">
      <input
        type="text"
        placeholder="Search"
        value={searchTerm}
        onChange={(e) => {getDebouncedHandler(e, (e) => {handleChange(e, searchTerm, setSearchTerm, setSearchResults); }, 100)}}
      />
      {renderSearchResults()}
    </div>
  );
}

export default Search;

The problem is, although I see my "getDebouncedHandler" method getting called, I don't think

_.debounce(handler, delay);

is doing anything because I never see the method listed there invoked. What else do I need to do to get debounce handler invoked properly?

Edit: Adding in the logic I'm using for "doSearch"

const doSearch = (query, searchTerm, setSearchResults) => {
  console.log("before fetch, with query:" + query);
  const searchUrl = "/coops/?contains=" + encodeURIComponent(query);
  fetch(searchUrl, {
    method: "GET",
  })
    .then((response) => response.json())
    .then((data) => {
      console.log("returning data for " + searchTerm + " query:" + query);
      console.log(data);
      if (query === searchTerm) {
        console.log("setting search results for search term:" + searchTerm);
        setSearchResults(data);
      }
    });
}

Solution

  • Here is one way to implement this :

    1. Don't debounce the onChange handler, this will cause a bad UX because some characters will be lost.
    2. Create a debounced version of your expensive function. Usually it's where you make a API call. I assume that's doSearch in your case.
    3. Keep the debounced function outside your component if your using a functional component, or use useMemo to keep the reference, otherwise it will be created every time, and it will not be very useful (it has an internal state to keep track of the last time it was called and other stuff ...).
    4. Keep in mind that when you call _.debounce it returns a function.
    5. Use useEffect to call your debounced function when the search query changes.

    The code :

    // Your expensive function
    const doSearch = (query, callback) => {
      const searchUrl = "/coops/?contains=" + encodeURIComponent(query);
      fetch(searchUrl, {
        method: "GET",
      })
        .then((response) => response.json())
        .then((data) => {
          callback(data);
        });
    }
    
    // The debounced version
    const doSearchDebounced = _.debounce(doSearch, 100);
    
    const Search = (props) => {
      const [searchTerm, setSearchTerm] = useState('');
      const [searchResults, setSearchResults] = useState([]);
    
      // Keep track of the last request ID
      const lastRequestId = useRef(null);
    
      // This hook will run when searchTerm changes
      useEffect(() => {
        if (!searchTerm) {
          setSearchResults([]);
          return;
        }
    
        // Update the last request ID
        const requestId = performance.now();
        lastRequestId.current = requestId;
    
        // Let the debounced function do it's thing
        doSearchDebounced(searchTerm, (results) => {
          // Only set the data if the current request is the last one
          if (requestId === lastRequestId.current) {
            setSearchResults(results);
          }
        });
    
      }, [searchTerm]);
    
      // No debounce here, just getting the user's input
      const handleChange = (e) => {
        setSearchTerm(e.target.value);
      };
    
      const renderSearchResults = ...
    
      return (
        <div className="searchForm">
          <input
            type="text"
            placeholder="Search"
            value={searchTerm}
            onChange={handleChange}
          />
          {renderSearchResults()}
        </div>
      );
    }