TypeOfNaN

Writing a Custom React useDebounce Hook with Typescript

By Nick Scialli on August 30, 2019

I started making free YouTube tutorials! If you like my blog, please check out my YouTube channel.

Introduction

In the past year, React released Hooks, which enable us to use state and other features in functional components rather than writing class components. We can also write custom hooks, which lets us abstract component logic into reusable functions. In this post, we’ll write a custom useDebounce hook using Typescript!

What is Debouncing?

Debouncing is a programming technique that enables us to defer the execution of a potentially-expensive operation rather than executing it constantly. A great example where this is helpful is filtering large lists based on user input. Rather than filtering on every keystroke, we can use debounce functionality to only apply the filter if the user hasn’t typed anything for a few seconds.

In the following screenshot, we show a large list of cities being filtered only after the user pauses typing for one second.

Cities being filtered

How Should Our Custom Hook Work?

There are a number of ways we could write our hook, but let’s make its usage look relatively similar to that of the setState hook:

const [value, setValue] = useState<string>('');

The difference is that we’ll want:

  1. To specify the debounce time
  2. To retrieve both the current value and the debounced value.

So it should look something like this:

const [debouncedValue, value, setValue] = useDebounce<string>('', 1000);

We could then use the useEffect hook to take some action based on debouncedValue changing:

const [debouncedValue, value, setValue] = useDebounce<string>('', 1000);

useEffect(() => {
  // Do something with the debounced value
  // e.g., setCities(cities.filter(el => el.name.includes(debouncedValue)))
}, [debouncedValue]);

Writing the Hook

useDebounce Types

To start off with, let’s create our useDebounce function and make sure we’ve defined its argument and return types correctly. We’ll want to make sure our hook takes a generic that provides the type for the initial value, value, and debounced value. Our two arguments, initalValue and time, will by the generic type and number type, respectively. Finally, our function will return a three element array. The first element (our debounced value) will be the generic type, the second element (our up-to-date/current value) will also be the generic type, and the final element will be our setted, a React.Dispatch type.

function useDebounce<T>(
  initialValue: T,
  time: number
): [T, T, React.Dispatch<T>] {}

Writing a Version Without Debouncing

Our hook will need to maintain two pieces of state: the up-to-date/current value and the debounced value. We will use the useState hook to maintain these states. To make a really simple version right now, we’re going to use the useEffect hook to determine when the up-to-date value changes and just immediately change the debounced value. In the future, we’ll deal with adding a delay to this process.

import { useState, useEffect } from 'react';

function useDebounce<T>(
  initialValue: T,
  time: number
): [T, T, React.Dispatch<T>] {
  const [value, setValue] = useState<T>(initialValue);
  const [debouncedValue, setDebouncedValue] = useState<T>(initialValue);

  useEffect(() => {
    setDebouncedValue(value);
  }, [value]);

  return [debouncedValue, value, setValue];
}

Adding Debounce Functionality

We can finally add our debouncing functionality. To do this, we can use a setTimeout within the useEffect. What’s key here is returning a function from useEffect that cancels the timeout. This function is essentially the componentWillUnmount equivalent for our effect!

import { useState, useEffect } from 'react';

function useDebounce<T>(
  initialValue: T,
  time: number
): [T, T, React.Dispatch<T>] {
  const [value, setValue] = useState<T>(initialValue);
  const [debouncedValue, setDebouncedValue] = useState<T>(initialValue);

  useEffect(() => {
    const debounce = setTimeout(() => {
      setDebouncedValue(value);
    }, time);
    return () => {
      clearTimeout(debounce);
    };
  }, [value, time]);

  return [debouncedValue, value, setValue];
}

Conclusion

That’s it! We can now use our custom hook to accomplish debouncing.


Nick Scialli

Nick Scialli is a software engineer at the U.S. Digital Service.

Get blog updates!

If you like what I post here, please sign up to get updates in your inbox. And don't worry, I won't spam you and you can unsubscribe any time!