Using Lodash debounce with React useCallback for input onChange event
Asked Answered
T

4

9

I'm trying to debounce an onChange event when a user type in a input field.

I'm referencing these threads:

I have the following snippet where I try to replicate the solutions provided in the threads above:

const handler = useCallback(debounce(setSearchQuery(value), 500), []);

useEffect(() => {
  document.addEventListener('keydown', handleDocumentKeyDown);
  handler(value);
  return () => document.removeEventListener('keydown', handleDocumentKeyDown);
}, [isOpen, handleDocumentKeyDown, handler, value]);

// ...

const handleChange = (event) => {
  setValue(event.target.value);
};

Error:

Uncaught TypeError: handler is not a function

How can I debounce setSerachQuery() for 500ms while the user is typing in the input field?

Tensity answered 19/5, 2020 at 8:18 Comment(0)
O
4

The issue in your case is that instead of passing a function to debounce, you are invoking it directly. You can use arrow function within debounce like

const handler = useCallback(debounce(() => setSearchQuery(value), 500), []);

Full code

const handler = useCallback(debounce(() => setSearchQuery(value), 500), []); // arrow function here

  useEffect(() => {
    document.addEventListener('keydown', handleDocumentKeyDown);
    handler(value);
    return () => document.removeEventListener('keydown', handleDocumentKeyDown);
  }, [isOpen, handleDocumentKeyDown, handler, value]);

  ...

  const handleChange = (event) => {
    setValue(event.target.value);
  };
Osteal answered 19/5, 2020 at 9:23 Comment(3)
this gives me the lint error React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.eslintreact-hooks/exhaustive-depsDepilate
you could pass setSearchQuery as a dependency to useCallback but since the function doesn't change, you can disable the eslint warning too. Check this post for more detailsOsteal
You cannot assume the function doesn't change and also disabling eslint warnings is a bad practice if you're working in a team, as you don't want newbie devs to copy this practice, bypassing eslint warning the second they get into difficulties. So, I suggest, spending as much time as possible solving the issuesKaki
P
7

Not pretty at all, but the only way I've managed to do it without dependency problems is:

  const debouncer = useRef(debounce((over: Over | null) => {
    console.log(over);
  }, 100));

  const onOver = useCallback((over: Over | null) => {
    debouncer.current(over);
  }, [debouncer]);
Pretypify answered 15/10, 2022 at 18:54 Comment(1)
Using a ref makes a lot of sense because we want to instantiate one copy of the debounce function that persists across renders. I don't think you need the useCallback at all, just use: const debouncer = useRef(....).current, then use the fuction as you normally would.Postfree
O
4

The issue in your case is that instead of passing a function to debounce, you are invoking it directly. You can use arrow function within debounce like

const handler = useCallback(debounce(() => setSearchQuery(value), 500), []);

Full code

const handler = useCallback(debounce(() => setSearchQuery(value), 500), []); // arrow function here

  useEffect(() => {
    document.addEventListener('keydown', handleDocumentKeyDown);
    handler(value);
    return () => document.removeEventListener('keydown', handleDocumentKeyDown);
  }, [isOpen, handleDocumentKeyDown, handler, value]);

  ...

  const handleChange = (event) => {
    setValue(event.target.value);
  };
Osteal answered 19/5, 2020 at 9:23 Comment(3)
this gives me the lint error React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.eslintreact-hooks/exhaustive-depsDepilate
you could pass setSearchQuery as a dependency to useCallback but since the function doesn't change, you can disable the eslint warning too. Check this post for more detailsOsteal
You cannot assume the function doesn't change and also disabling eslint warnings is a bad practice if you're working in a team, as you don't want newbie devs to copy this practice, bypassing eslint warning the second they get into difficulties. So, I suggest, spending as much time as possible solving the issuesKaki
B
2

I recommend using useMemo() instead, should be something like this:

const handler = useMemo(() => debounce(
  (value) => setSearchQuery(value),
  500,
), []);

Source:

Bisayas answered 27/1 at 1:42 Comment(0)
J
0

I'm also using a ref here. The reasoning is, react demands we use an inline function in useCallback, so debounce just cannot fit in the useCallback statement. So we need to sync the debounced version with useCallback's result. Which leads to a useEffect.

(In my case I'm using redux-toolkit so dispatch is the dependency, and I need to debounce the request sender triggered by user input.)

const debouncedFetcherRef = useRef((_input: string) => {});

const fetcher = useCallback(
  (input: string) => {
    dispatch(sendRequest({ search: input }))
  },
  [dispatch]
);

useEffect(() => {
  debouncedFetcherRef.current = debounce(fetcher, 500);
}, [fetcher]);

<Input onInput={event => {
  debouncedFetcherRef.current(event.target.value);
}}/>
Justificatory answered 18/1 at 10:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.