March 23, 2021

Debounce and Throttle Callbacks with React Hooks

edit

This one will be short and to the point. I recently ran into a problem while trying to create a debounced callback with React hooks. If you’re using the react-hooks ESLint package with the recommended settings, then it will warn you that you can’t do the following:

import React from 'react'
import { debounce } from 'lodash'

function Search({ onSearch }) {
  const [value, setValue] = React.useState('')

  // This use of `useCallback` has a problem
  const debouncedSearch = React.useCallback(
    debounce(val => {
      onSearch(val)
    }, 750),
    [onSearch]
  )

  const handleChange = React.useCallback(
    e => {
      setValue(e.target.value)
      debouncedSearch(e.target.value)
    },
    [debouncedSearch]
  )

  return <input type="text" value={value} onChange={handleChange} />
}

This looks fine, but here’s the warning:

Error: React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead react-hooks/exhaustive-deps

useCallback expects an inline function. Handing it the returned function from a debounce or throttle doesn’t cut the mustard. Why? The ESLint rule can’t figure out what the exhaustive dependencies should be. How do we fix this?

It might seem odd, but useMemo comes to the rescue.

function Search({ onSearch }) {
  const [value, setValue] = React.useState('')

  // This is the solution
  const debouncedSearch = React.useMemo(
    () =>
      debounce(val => {
        onSearch(val)
      }, 750),
    [onSearch]
  )

  const handleChange = React.useCallback(
    e => {
      setValue(e.target.value)
      debouncedSearch(e.target.value)
    },
    [debouncedSearch]
  )

  return <input type="text" value={value} onChange={handleChange} />
}

Think about it. useMemo is used to store a memoized value that should only be recalculated when the dependencies change. So what if that memoized value is a function?

It wasn’t obvious to me to do this at first, but it makes sense. We want the returned function, and we want it to have the correct exhaustive dependencies, and update whenever they change. useMemo does exactly that.

You can use this same technique with throttle, too. Really any higher ordered function that returns a function. Sky is the limit.

Lastly, credit where it’s due. I came across this idea from this GitHub comment. Maybe give it a thumbs up if you found this helpful.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Just Enough Functional Programming
Just Enough Functional Programming
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.