1

I have a search input in one of my page and I'm trying to make sure that it does not make 10 requests in one second if the user types too fast.

Seems like debouncing is the way to go. I've read multiple blog posts and SO questions. I'm trying to use debounce from lodash. I must be doing something wrong since what happens is that all my call are passed, just later.

Here is my component code:

const Partners = (props: any) => {
  const [query, setQuery] = useState("");
  const { partners, totalPages, currentPage } = props.partnersList;

  useEffect(() => {
    props.getPartners();
  }, []);

  useEffect(() => debounce(() => props.getPartners(query), 10000), [query]);

  const handleQueryChange = (e: any) => {
    setQuery(e.target.value);
  };

  const handlePageChange = (e: React.ChangeEvent<unknown>, value: number) => {
    props.getPartners(query, value);
  };

  return (
    <React.Fragment>
      <Sidebar />
      <MainContent className="iamSearchPartners">
        <h1>Partners</h1>
        <TextField
          className="iamSearchPartners__search_input"
          variant="outlined"
          size="small"
          onChange={handleQueryChange}
          value={query}
        />
        <PartnersList partners={partners} />
        {totalPages > 1 && (
          <Pagination
            currentPage={currentPage || 1}
            totalPages={totalPages}
            handlePageChange={handlePageChange}
          />
        )}{" "}
      </MainContent>
    </React.Fragment>
  );
};

As you can see, I have a useEffect listening to changes to query.

Jean-Baptiste
  • 1,552
  • 1
  • 19
  • 33
  • This is by far one of the best library I found for the debounce event: https://github.com/xnimorz/use-debounce check it out. Also having a defaultValue={curretState} will significantly improve your performance which is a plus. – Elvis S. Jul 11 '20 at 22:35

2 Answers2

1

I end up doing this manually honestly. Check out the differences between debouncing and throttling. I think you want to throttle the requests. If you debounce, nothing is going to happen until the timer ends.

If you want to wait 10 seconds as in your example, nothing should happen if the user types at least once every ten seconds, until 10 seconds after the last type. As opposed to throttling where a request would go out every 10 seconds.

This approach is kind of a hybrid because we are making sure the last one still goes out (as a debounce would), and throttling may not do, but we are still sending requests while the user is typing.

  const timer = useRef<number>(0);
  const lastEventSent = useRef(Date.now());

  useEffect(() => {
     if (Date.now() - lastEventSent.current < 500) {

     // The event was fired too recently, but we still want
     // to fire this event after the timeout if it is the last
     // one (500ms in this case).
      timer.current = setTimeout(() => {
          lastEventSent.current = Date.now();
          props.getPartners(query)
      }, 500);

     } else {

       // An event hasn't been fired for 500ms, lets fire the event
       props.getPartners(query)
       lastEventSent.current = Date.now();

       // And if the user was typing, there is probably a timer, lets clear that
       clearTimeout(timer.current);
     }
  
     // Cleanup the timer if there was one and this effect is reloaded.
     return () => clearTimeout(timer.current);

   }, [query]);
Diesel
  • 5,099
  • 7
  • 43
  • 81
1

debounce creates a debounced version of the function that passed as the argument. In this specific case, calling it in useEffect will create a new debounced version of the function on every render.

To mitigate this, the debounced version could be created outside of the render function, so it is not recreated on every render.

const myDebouncedFunction = debounce((handler, query) => handler(query), 10000);

const Partners = (props: any) => {
  // omitted ...
  useEffect(() => {
    myDebouncedFunction(props.getPartners, query);
  }, [query]);
  // omitted ...

useMemo or putting it into a useState could work as well.

Another thing: useEffect only called the returned debounced version since it's an arrow function without braces, making it return the result of the evaluation. useEffect leverages the return of it's function to be some kind of "unsubscribe" handler, see React docs about "Effects with cleanup". So whenever query changed (after the second time), the effect was calling the function. The version above should be called on every query change.

Personally, I would try to handle the call of the debounced version of the function in the handleQueryChange instead of a useEffect, to make the "when should it happen" more clear.

Narigo
  • 2,979
  • 3
  • 20
  • 31
  • Calling the function from outside the component made it work. I also called the debounced function from the handleQueryChange instead. It's clearer this way. Much thanks! – Jean-Baptiste Jul 12 '20 at 00:39