How to debounce and throttle in React without losing your mind
Tags: javascript, webdev, react, performance
Published: 2023-01-12 08:00:00


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

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

When talking about performance in general, and especially in React, the words “immediate”, “fast”, “as soon as possible” instantly come to mind. Is it always true though? Contrary to common wisdom, sometimes it’s actually good to slow down 
and think about life
. Slow and steady wins the race, you know 😉

The last thing that you want is an async search functionality to crash your web server, just because a user is typing too fast and you send requests on every keystroke. Or your app to become unresponsive or even crash your browser window during scroll, just because you’re doing expensive calculations on every scroll event fired (there can be 30-100 per second of those!).

This is when such “slow down” techniques as “throttle” and “debounce” come in handy. Let's take a brief look at what they are (in case you haven’t heard of them yet), and then focus on how to use them in React correctly - there are a few caveats there that a lot of people are not aware of!

Side note: I’m going to use [lodash](https://lodash.com/) library’s [debounce](https://lodash.com/docs/4.17.15#debounce) and [throttle](https://lodash.com/docs/4.17.15#throttle) functions. Techniques and caveats, described in the article, are relevant to any library or implementation, even if you decide to implement them by yourself.

<h2 id="part1">What is debouncing and throttling</h2>

[Debouncing and throttling](https://css-tricks.com/debouncing-throttling-explained-examples/) are techniques that allow us to skip function execution if that function is called too many times over a certain time period.

Imagine, for example, that we’re implementing a simple asynchronous search functionality: an input field, where a user can type something, text that they type is sent to the backend, which in turn returns relevant search results. We can surely implement it “naively”, just an input field and `onChange` callback:

```tsx
const Input = () => {
  const onChange = (e) => {
    // send data from input field to the backend here
    // will be triggered on every keystroke
  }
  return <input onChange={onChange} />
}
```

But a skilled typer can type with the speed of 70 words per minute, which is roughly 6 keypresses per second. In this implementation, it will result in 6 `onChange` events, i.e. 6 requests to the server per second! Sure your backend can handle that?

Instead of sending that request on every keypress, we can wait a little bit until the user stops typing, and then send the entire value in one go. This is what debouncing does. If I apply debounce to my `onChange` function, it will detect every attempt I make to call it, and if the waiting interval hasn’t passed yet, it will drop the previous call and restart the “waiting” clock.

```tsx
const Input = () => {
  const onChange = (e) => {
    // send data from input field to the backend here
    // will be triggered 500 ms after the user stopped typing
  }

  const debouncedOnChange = debounce(onChange, 500);

  return <input onChange={debouncedOnChange} />
}
```

Before, if I was typing “React” in the search field, the requests to the backend would be on every keypress instantaneously, with the values “R”, “Re”, “Rea”, “Reac”, “React”. Now, after I debounced it, it will wait 500 ms after I stopped typing “React” and then send only one request with the value “React”.

Underneath, `debounce` is just a function, that accepts a function, returns another function, and has a tracker inside that detects whether the passed function was called sooner than the provided interval. If sooner - then skip the execution and re-start the clock. If the interval passed - call the passed function. Essentially it’s something like this:

```tsx
const debounce = (callback, wait) => {
  // initialize the timer
  let timer;

  ...
    // lots of code involving the actual implementation of timer
    // to track the time passed since the last callback call
  ...

  const debouncedFunc = () => {
    // checking whether the waiting time has passed
    if (shouldCallCallback(Date.now())) {
      callback();
    } else {
      // if time hasn't passed yet, restart the timer
      timer = startTimer(callback);
    }
  }

  return debouncedFunc;
}
```

The actual implementation is of course a bit more complicated, you can check out lodash [debounce code](https://github.com/lodash/lodash/blob/master/debounce.js) to get a sense of it.

`Throttle` is very similar, and the idea of keeping the internal tracker and a function that returns a function is the same. The difference is that `throttle` guarantees to call the callback function regularly, every `wait` interval, whereas `debounce` will constantly reset the timer and wait until the end.

The difference will be obvious if we use not an async search example, but an editing field with auto-save functionality: if a user types something in the field, we want to send requests to the backend to save whatever they type “on the fly”, without them pressing the “save” button explicitly. If a user is writing a poem in a field like that really really fast, the “debounced” `onChange` callback will be triggered only once. And if something breaks while typing, the entire poem will be lost. “Throttled” callback will be triggered periodically, the poem will be regularly saved, and if a disaster occurs, only the last milliseconds of the poem will be lost. Much safer approach.

You can play around with “normal” input, debounced input, and throttled input fields in this example:

{% embed https://codesandbox.io/embed/example-1-simplest-debounce-throttle-8gktph?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}

<h2 id="part2">Debounced callback in React: dealing with re-renders</h2>

Now, that it’s a bit more clear what are debounce and throttle, why we need them, and how they are implemented, it’s time to dig deep into how they should be used in React. And I hope you don’t think now “Oh c’mon, how hard can it be, it’s just a function”, do you? It’s React we’re talking about, when it was ever that easy? 😅

First of all, let's take a closer look at the `Input` implementation that has debounced `onChange` callback (from now forward I’ll be using only `debounce` in all examples, every concept described will be relevant for throttle as well).

```tsx
const Input = () => {
  const onChange = (e) => {
    // send data from input to the backend here
  }

  const debouncedOnChange = debounce(onChange, 500);

  return <input onChange={debouncedOnChange} />
}
```

While the example works perfectly, and seems like a regular React code with no caveats, it unfortunately has nothing to do with real life. In real life, more likely than not, you’d want to do something with the value from the input, other than sending it to the backend. Maybe this input will be part of a large form. Or you’d want to introduce a “clear” button there. Or maybe the `input` tag is actually a component from some external library, which mandatory asks for the `value` field.

What I’m trying to say here, at some point you’d want to save that value into state, either in the `Input` component itself, or pass it to parent/external state management to manage it instead. Let’s do it in `Input`, for simplicity.

```tsx
const Input = () => {
  // adding state for the value
  const [value, setValue] = useState();

  const onChange = (e) => {};
  const debouncedOnChange = debounce(onChange, 500);

  // turning input into controlled component by passing value from state there
  return <input onChange={debouncedOnChange} value={value} />
}
```

I added state `value` via `useState` hook, and passed that value to `input` field. One thing left to do is for `input` to update that state in its `onChange` callback, otherwise, input won’t work. Normally, without debounce, it would be done in `onChange` callback:

```tsx
const Input = () => {
  const [value, setValue] = useState();

  const onChange = (e) => {
    // set state value from onChange event
    setValue(e.target.value);
  };

  return <input onChange={onChange} value={value} />
}
```

I can’t do that in `onChange` that is debounced: its call is by definition delayed, so `value` in the state won’t be updated on time, and `input` just won’t work.

```tsx
const Input = () => {
  const [value, setValue] = useState();

  const onChange = (e) => {
    // just won't work, this callback is debounced
    setValue(e.target.value);
  };
  const debouncedOnChange = debounce(onChange, 500);

  return <input onChange={debouncedOnChange} value={value} />
}
```

I have to call `setValue` immediately when `input` calls its own `onChange`. This means I can’t debounce our `onChange` function anymore in its entirety and only can debounce the part that I actually need to slow down: sending requests to the backend.

Probably something like this, right?

```tsx
const Input = () => {
  const [value, setValue] = useState();

  const sendRequest = (value) => {
    // send value to the backend
  };

  // now send request is debounced
  const debouncedSendRequest = debounce(sendRequest, 500);

  // onChange is not debounced anymore, it just calls debounced function
  const onChange = (e) => {
    const value = e.target.value;

    // state is updated on every value change, so input will work
    setValue(value);

    // call debounced request here
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}
```

Seems logical. Only… It doesn’t work either! Now the request is not debounced at all, just delayed a bit. If I type “React” in this field, I will still send all “R”, “Re”, “Rea”, “Reac”, “React” requests instead of just one “React”, as properly debounced func should, only delayed by half a second.

Check out both of those examples and see for yourself. Can you figure out why?

{% embed https://codesandbox.io/embed/example-2-controlled-component-and-debouncing-broken-chxi6v?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}

The answer is of course re-renders (it usually is in React 😅). As we know, one of the [main reasons a component re-renders](https://www.developerway.com/posts/react-re-renders-guide#part2.1) is a state change. With the introduction of state to manage value, we now re-render the entire `Input` component on every keystroke. As a result, on every keystroke, we now call the actual `debounce` function, not just the debounced callback. And, as we know from the previous chapter, the `debounce` function when called, is:

* creating a new timer
* creating and returning a function, inside of which the passed callback will be called when the timer is done

So when on every re-render we’re calling `debounce(sendRequest, 500)`, we’re re-creating everything: new call, new timer, new return function with callback in arguments. But the old function is never cleaned up, so it just sits there in memory and waits for its own timer to pass. When its timer is done, it fires the callback function, and then just dies and eventually gets cleaned up by the garbage collector.

What we ended up with is just a simple `delay` function, rather than a proper `debounce`. The fix for it should seem obvious now: we should call `debounce(sendRequest, 500)` only once, to preserve the inside timer and the returned function.

The easiest way to do it would be just to move it outside of `Input` component:

```tsx
const sendRequest = (value) => {
  // send value to the backend
};
const debouncedSendRequest = debounce(sendRequest, 500);

const Input = () => {
  const [value, setValue] = useState();

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);

    // debouncedSendRequest is created once, so state caused re-renders won't affect it anymore
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}
```

This won’t work, however, if those functions have dependencies on something that is happening within component’s lifecycle, i.e. state or props. No problem though, we can use memoization hooks to achieve exactly the same result:

```tsx
const Input = () => {
  const [value, setValue] = useState("initial");

  // memoize the callback with useCallback
  // we need it since it's a dependency in useMemo below
  const sendRequest = useCallback((value: string) => {
    console.log("Changed value:", value);
  }, []);

  // memoize the debounce call with useMemo
  const debouncedSendRequest = useMemo(() => {
    return debounce(sendRequest, 1000);
  }, [sendRequest]);

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);
    debouncedSendRequest(value);
  };

  return <input onChange={onChange} value={value} />;
}
```

Here is the example:

{% embed https://codesandbox.io/embed/example-3-controlled-component-and-debouncing-fixed-telhkh?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}

Now everything is working as expected! `Input` component has state, backend call in `onChange` is debounced, and debounce actually behaves properly 🎉

Until it doesn’t…

<h2 id="part3">Debounced callback in React: dealing with state inside</h2>

Now to the final piece of this bouncing puzzle. Let’s take a look at this code:

```tsx
const sendRequest = useCallback((value: string) => {
  console.log("Changed value:", value);
}, []);
```

Normal memoized function, that accepts `value` as an argument and then does something with it. The value is coming directly from `input` through debounce function. We pass it when we call the debounced function within our `onChange` callback:

```tsx
const onChange = (e) => {
  const value = e.target.value;
  setValue(value);

  // value is coming from input change event directly
  debouncedSendRequest(value);
};
```

But we have this value in state as well, can’t I just use it from there? Maybe I have a chain of those callbacks and it's really hard to pass this value over and over through it.  Maybe I want to have access to another state variable, it wouldn’t make sense to pass it through a callback like this. Or maybe I just hate callbacks and arguments, and want to use state just because. Should be simple enough, isn’t it?

And of course, yet again, nothing is as simple as it seems. If I just get rid of the argument and use the `value` from state, I would have to add it to the dependencies of `useCallback` hook:

```tsx
const Input = () => {
  const [value, setValue] = useState("initial");

  const sendRequest = useCallback(() => {
    // value is now coming from state
    console.log("Changed value:", value);

    // adding it to dependencies
  }, [value]);

}
```

Because of that, `sendRequest` function will change on every value change - that’s how memoization works, the value is the same throughout the re-renders until the dependency changes. This means our memoized debounce call will now change constantly as well - it has `sendRequest` as a dependency, which now changes with every state update.

```tsx
// this will now change on every state update
  // because sendRequest has dependency on state
  const debouncedSendRequest = useMemo(() => {
    return debounce(sendRequest, 1000);
  }, [sendRequest]);
```

And we returned to where we were the first time we introduced state to the `Input` component: debounce turned into just delay.

[See example in code sandbox](https://codesandbox.io/s/example-4-state-inside-callback-wufvem)

Is there anything that can be done here?

If you search for articles about debouncing and React, half of them will mention `useRef` as a way to avoid re-creating the debounced function on every re-render. `useRef` is a useful hook that allows us to create `ref` - a mutable object that is persistent between re-renders. `ref` is just an alternative to memoization in this case.

Usually, the pattern goes like this:

```tsx
const Input = () => {
  // creating ref and initializing it with the debounced backend call
  const ref = useRef(debounce(() => {
    // this is our old "debouncedSendRequest" function
  }, 500));

  const onChange = (e) => {
    const value = e.target.value;

    // calling the debounced function
    ref.current();
  };
}
```

This might be actually a good alternative to the previous solution based on `useMemo` and `useCallback`. I don’t know about you, but those chains of hooks give me a headache and make my eye twitch. Impossible to read and understand! The ref-based solution seems much easier.

Unfortunately, this solution will only work for the previous use-case: when we didn’t have state inside the callback. Think about it. The `debounce` function here is called only once: when the component is mounted and `ref` is initialized. This function creates what is known as “[closure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)”: the outside data that was available to it when it was created will be preserved for it to use. In other words, if I use state `value` in that function:

```tsx
const ref = useRef(debounce(() => {
    // this value is coming from state
    console.log(value);
  }, 500));
```

the value will be “frozen” at the time the function was created - i.e. initial state value. When implemented like this, if I want to get access to the latest state value, I need to call the `debounce` function again in `useEffect` and re-assign it to the ref. I can’t just update it. The full code would look something like this:

```tsx
const Input = () => {
  const [value, setValue] = useState();

  // creating ref and initializing it with the debounced backend call
  const ref = useRef(debounce(() => {
    // send request to the backend here
  }, 500));

  useEffect(() => {
    // updating ref when state changes
    ref.current = debounce(() => {
      // send request to the backend here
    }, 500);
  }, [value]);

  const onChange = (e) => {
    const value = e.target.value;

    // calling the debounced function
    ref.current();
  };
}
```

But unfortunately, this is no different than `useCallback` with dependencies solution: the debounced function is re-created every time, the timer inside is re-created every time, and debounce is nothing more than re-named `delay`.

See for yourself:

{% embed https://codesandbox.io/embed/example-5-state-inside-callback-with-ref-q1r67v?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}

But we’re actually onto something here, the solution is close, I can feel it.

One thing that we can take advantage of here, is that in Javascript objects are **not** [immutable](https://developer.mozilla.org/en-US/docs/Glossary/Immutable). Only primitive values, like numbers or references to objects, will be “frozen” when a closure is created. If in our “frozen” `sendRequest` function I will try to access `ref.current`, which is by definition mutable, I will get the latest version of it all the time!

Let’s recap: `ref` is mutable; I can only call `debounce` function once on mount; when I call it, a closure will be created, with primitive values from the outside like `state` value "frozen" inside; mutable objects will not be “frozen”.

And hence the actual solution: attach the non-debounced constantly re-created `sendRequest` function to the ref; update it on every state change; create “debounced” function only once; pass to it a function that accesses `ref.current` - it will be the latest sendRequest with access to the latest state.

Thinking in closures breaks my brain 🤯, but it actually works, and easier to follow that train of thought in code:

```tsx
const Input = () => {
  const [value, setValue] = useState();

  const sendRequest = () => {
    // send request to the backend here
    // value is coming from state
    console.log(value);
  };

  // creating ref and initializing it with the sendRequest function
  const ref = useRef(sendRequest);

  useEffect(() => {
    // updating ref when state changes
    // now, ref.current will have the latest sendRequest with access to the latest state
    ref.current = sendRequest;
  }, [value]);

  // creating debounced callback only once - on mount
  const debouncedCallback = useMemo(() => {
    // func will be created only once - on mount
    const func = () => {
      // ref is mutable! ref.current is a reference to the latest sendRequest
      ref.current?.();
    };
    // debounce the func that was created once, but has access to the latest sendRequest
    return debounce(func, 1000);
    // no dependencies! never gets updated
  }, []);

  const onChange = (e) => {
    const value = e.target.value;

    // calling the debounced function
    debouncedCallback();
  };
}
```

Now, all we need to do is to extract that mind-numbing madness of closures in one tiny hook, put it in a separate file, and pretend not to notice it 😅

```tsx
const useDebounce = (callback) => {
  const ref = useRef();

  useEffect(() => {
    ref.current = callback;
  }, [callback]);

  const debouncedCallback = useMemo(() => {
    const func = () => {
      ref.current?.();
    };

    return debounce(func, 1000);
  }, []);

  return debouncedCallback;
};
```

And then our production code can just use it, without the eye-bleeding chain of `useMemo` and `useCallback`, without worrying about dependencies, and with access to the latest state and props inside!

```tsx
const Input = () => {
  const [value, setValue] = useState();

  const debouncedRequest = useDebounce(() => {
    // send request to the backend
    // access to latest state here
    console.log(value);
  });

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);

    debouncedRequest();
  };

  return <input onChange={onChange} value={value} />;
}
```

Isn’t that pretty?  You can play around with the final code here:

{% embed https://codesandbox.io/embed/example-6-final-eample-with-ref-9uhc27?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}

<h2 id="part4">Before you bounce</h2>

Hope this bouncing around was useful for you and now you feel more confident in what debounce and throttle are, how to use them in React, and what are the caveats of every solution.

Don’t forget: `debounce` or `throttle` are just functions that have an internal time tracker. Call them only once, when the component is mounted. Use such techniques as memoization or creating a `ref` if your component with debounced callback is subject to constant re-renders. Take advantage of javascript closures and React `ref` if you want to have access to the latest state or props in your debounced function, rather than passing all the data via arguments.

May the force never bounce away from you✌🏼

---

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.