reactjslodashreact-hooksthrottling

How to use throttle or debounce with React Hook?


I'm trying to use the throttle method from lodash in a functional component, e.g.:

const App = () => {
  const [value, setValue] = useState(0)
  useEffect(throttle(() => console.log(value), 1000), [value])
  return (
    <button onClick={() => setValue(value + 1)}>{value}</button>
  )
}

Since the method inside useEffect is redeclared at each render, the throttling effect does not work.

Does anyone have a simple solution (without moving the throttle implementation outside of the component) ?


Solution

  • From the 2025 the best pattern I saw yet is(and other users noticed that below, please upvote their answers not mine):

      const [search, setSearch] = useState('')
    
      const onSearch = useMemo(
        () =>
          debounce((phrase: string) => {
            setSearch(phrase)
          }, SEARCH_DELAY),
        [],
      )
    
      useEffect(() => {
        return () => onSearch.cancel()
      }, [onSearch])
    

    For controlled component, though, we don't want to have a delay between keypress and character appearing in the field. In this case you may have two different state - one for the field value which is updated immediately and another - debounced - for a search value.

    There were other users were noticing this approach, but my answer is listed first and I want to communicate that old answer is not relevant any more(or have never been?).

    original answer below

    you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

    Something like that

    const App = () => {
      const [value, setValue] = useState(0)
      const throttled = useRef(throttle((newValue) => console.log(newValue), 1000))
      
      useEffect(() => throttled.current(value), [value])
      
      return (
        <button onClick={() => setValue(value + 1)}>{value}</button>
      )
    }
    

    As for useCallback:

    It may work too as

    const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []);
    

    But if we try to recreate callback once value is changed:

    const throttled = useCallback(throttle(() => console.log(value), 1000), [value]);
    

    we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

    So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

    [UPD] initially it was

      const throttled = useRef(throttle(() => console.log(value), 1000))
      
      useEffect(throttled.current, [value])
    

    but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

    So be careful while pushing functions into useRef because of closure feature.