skip to Main Content

I’m trying to use debounce in lodash to delay the onChange, see the code below.

import React, { useState, useEffect, useCallback } from "react";
import { TopBar } from "@shopify/polaris";
import { debounce } from "lodash";

function SearchBar() {
  const [searchValue, setSearchValue] = useState("");

  const handleSearchFieldChange = ((value:string) => {
    setSearchValue(value);
  });

  const debounceLoadData = useCallback(debounce({searchValue} => fetchData, 1000), []);

  useEffect(() => {
    debounceLoadData();
    console.log({searchValue})
  }, [searchValue]);

  function fetchData(value:string) {
    console.log("searchValue " + value);
  }

  const searchFieldMarkup = (
    <TopBar.SearchField
      onChange={handleSearchFieldChange}
      value={searchValue}
      placeholder="Search Value"
    />
  );

  return <TopBar searchField={searchFieldMarkup} />;
}

In the beginning, I was tring to use searchValue in fetchData function but seems because of scope, it failed to read it, it was always empty though the state had been updated.

As a result, I try to pass it in from the debounceLoadData but I don’t know how I can do that as what in useCallback is a function invocation. How can I pass searchValue in fetchData inside debounce.

3

Answers


  1. I think you are getting confused by the functional setState syntax. Try this:

    const debounceLoadData = useCallback(() => debounce(() => fetchData(searchValue), 1000), []);
    
    Login or Signup to reply.
  2. lodash debounce takes in a function as the first argument. You can simply use fetchData as the function and pass on the searchValue to debounceLoadData which will then be passed to fetchData on invocation

    const debounceLoadData = useCallback(debounce(fetchData, 1000), []);
    
      useEffect(() => {
        debounceLoadData(searchValue);
        console.log({searchValue})
      }, [searchValue]);
    

    debounce actually returns a function, think of debounce as being implemented like

    function debounce(func, wait) {
      let timeout
      return function(...args) {
        const context = this
        clearTimeout(timeout)
        timeout = setTimeout(() => func.apply(context, args), wait)
      }
    }
    

    So basically debounceLoadData is the returned function here and arguments passed to it i.e ...args are being then passed to original function fetchData like func.apply(context, args)

    Also debounceLoadData is created only once as the callback dependency is [], you whether you pass it to useEffect as a dependency of not will not make any difference.

    Please read this post for missing dependency warning

    How to fix missing dependency warning when using useEffect React Hook?

    Login or Signup to reply.
  3. I never like useCallback, it’s quite a confusing hook, and I always use useMemo instead since it totally covers what useCallback can do (but not the other way around).

    function SearchBar() {
      const [searchValue, setSearchValue] = useState("");
    
      const handleSearchFieldChange = ((value:string) => {
        setSearchValue(value);
      });
    
      const debounceLoadData = useMemo(() => debounce(fetchData, 1000), []);
      /**
       * the equivalent of useCallback should be:
       *
       * const debounceLoadData = useCallback(debounce(fetchData, 1000), []);
       * 
       * But I really advice against it!
       * There's unnecessary function invocation compared to useMemo.
       */
    
    
      useEffect(() => {
        debounceLoadData(searchValue);  // <- you should pass in arg
        console.log({searchValue})
      }, [searchValue]);
    
      // ...
    }
    

    Yet for your case I don’t think using lodash debounce is the best solution.

    There’s a hidden risk that the final invocation of effect fetchData happens AFTER your component is unmounted. And if fetchData contains some state mutation logic, that would raise an error of “Can’t call setState (or forceUpdate) on an unmounted component.” which is not destructive but not optimal either.

    I suggest manually debounce call using setTimeout/clearTimeout. It’s pretty simple:

    useEffect(() => {
      const timeoutId = setTimeout(() => fetchData(searchValue), 1000)
      return () => clearTimeout(timeoutId)
    }, [searchValue])
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search