Debounce onChange callback using a custom React hook
Tags: react, javascript, tutorial, frontend
Published: 2023-02-11 12:52:34

# Intro

Let's say you have a search component with a suggest. Suggest items are fetched as you type. You don't want to fetch items on every keystroke, you want to fetch when the user stops typing. How to do this? You probably want to use a technique called 'debouncing'.

# What is debouncing?

Debouncing is a technique to limit the rate at which a function is being called. It is commonly used to improve performance and user experience when working with events that trigger rapid function calls.

One common example of when debouncing is needed is when dealing with user inputs, such as typing in a search field. If you were to make a server request for every character that the user types, it could lead to a large number of unnecessary requests and slow down the app.

In this article I will explain how to reduce number of network requests using debouncing. We'll also extract debouncing logic into a custom React hook which can be reused. 

# Example

Let's take a look at a practical example. Below there's a form for searching a movie by its name. Movie names are fetched from server (in this example we'll be using a mock instead of a real server, but it doesn't make much difference). Try searching 'godfather' in the input below, notice the output in the console:

{% embed https://codesandbox.io/embed/step-1-no-debounce-1uoorx?expanddevtools=1&fontsize=14&hidenavigation=1&theme=light %}

As you see, the app is making a server request after each keystroke:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gn1wmrn5bqfsrbwsmhvm.png)

Let's see how we can improve this!

## Step 1: add `setTimeout`

Let's take a look at our Input component. Currently it renders the `value` provided by it's parent component, and calls `onChange` every time user changes the value:

```js
function Input({ value, onChange }) {
  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
```

We don't want to call `onChange` instantly, let's try to wrap it in a `setTimeout`:

```js
function Input({ value, onChange }) {
  // This callback calls 'onChange' after a timeout
  const debouncedOnChange = (val) => {
    setTimeout(() => onChange(val), 1000);
  };

  return (
    <input value={value} onChange={(e) => debouncedOnChange(e.target.value)} />
  );
}
```

Give it a try:

{% embed https://codesandbox.io/embed/step-2-settimeout-j6q0vn?expanddevtools=1&fontsize=14&hidenavigation=1&theme=light %}

This clearly adds a delay, but doesn't work as expected! Notice how user input is delayed. Looks like `onChange` should be called with a delay, but input value should update instantly, without an delay. How can we achieve this?

## Step 2: add local state

We can add local state to our component to store current user input. We'll update local state instantly, and call `onChange` with a delay. Here's what it might look like:

```js
function Input({ value, onChange }) {
  // Store user input in local state. Initial value is provided by parent component
  const [localValue, setLocalValue] = useState(value);

  const debouncedOnChange = (val) => {
    // Set local value instantly
    setLocalValue(val);
    
    // Set parent value after a delay
    setTimeout(() => onChange(val), 1000);
  };

  return (
    <input value={localValue} onChange={(e) => debouncedOnChange(e.target.value)} />
  );
}
```

{% embed https://codesandbox.io/embed/step-3-settimeout-state-ckzqgn?expanddevtools=1&fontsize=14&hidenavigation=1&theme=light %}

It works great! But looks like it still makes a network request on every key stroke, just the requests are delayed now. Let's fix this!

## Step 3: add clearTimeout

If user changes input during the timeout, we want to cancel scheduled `onChange` call and schedule another one, with a new input value. Each time we call `setTimeout`, it returns a unique number called timeout id. We can pass this number to `clearTimeout` to cancel the scheduled timeout and then schedule a new one. To store timeout id we can use `useRef` hook. Here's what it can look like:

```js
function Input({ value, onChange }) {
  const [localValue, setLocalValue] = useState(value);

  // Stores reference to timeout id
  const timeoutRef = useRef(null);

  const debouncedOnChange = (val) => {
    // Clear timeout if there was one pending
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    // Set local value instantly
    setLocalValue(val);

    // Schedule `onChange` call and store timeout id in the ref
    timeoutRef.current = setTimeout(() => onChange(val), 1000);
  };

  return (
    <input
      value={localValue}
      onChange={(e) => debouncedOnChange(e.target.value)}
    />
  );
}
```

{% embed https://codesandbox.io/embed/step-4-cleartimeout-tuvq2i?expanddevtools=1&fontsize=14&hidenavigation=1&theme=light %}

Brilliant! It does the job!

## Step 5: extracting to a custom hook

Notice how bloated `Input` component has become. Let's extract debounce related logic to a custom hook. This way our component code will be shorter and easier to maintain, and debounce logic can be reused. Here's what it might look like:

```js
// This custom hook can transform any callback to a debounced version
function useDebounce(onChange, duration) {
  const timeoutRef = useRef(null);
  const onEdit = useCallback(
    (val) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
      timeoutRef.current = setTimeout(() => onChange(val), duration);
    },
    [duration, onChange]
  );
  return onEdit;
}

function Input({ value, onChange }) {
  const [text, setText] = useState(value);
  // Custom hook accepts two arguments: original onChange callback, and debounce delay. Its return value is a new, debounced callback
  const debouncedOnChange = useDebounce(onChange, 1000);
  const onEdit = (val) => {
    setText(val);
    debouncedOnChange(val);
  };
  return <input value={text} onChange={(e) => onEdit(e.target.value)} />;
}
```

Here you can play around with the final version

{% embed https://codesandbox.io/embed/step-5-custom-hook-fkltlv?expanddevtools=1&fontsize=14&hidenavigation=1&theme=light %}

## Conclusion

We have introduced debouncing to our component to limit number of network requests. We've 'packed' debounce code into a custom hook, so that it can be reused elsewhere.

Thank you for following my tutorial!

> Cover photo by <a href="https://unsplash.com/@wgzhai?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Wengang Zhai</a> on <a href="https://unsplash.com/photos/MJ_0PxIuquI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>