How to Use Lodash Debounce & Lodash Throttle in React

Lodash provides two powerful utility functions, debounce
and throttle
, that help optimize performance when dealing with high-frequency events such as scrolling, resizing, and typing. In this blog post, we will explore how to use these functions effectively in a React application.
TL;DR
If you're looking for a quick answer, jump straight to the conclusion section at the end of this blog post.
Prerequisites:
You should have a basic understanding of React. Familiarity with hooks like useCallback
is recommended but not required. To understand the concepts of debouncing and throttling, refer to this article written by David Corbacho on CSS-Tricks: Debouncing and Throttling Explained.
For a quick overview:
Debouncing: Ensures that a function executes only after a specified delay has passed since the last time it was invoked. This is useful for reducing unnecessary computations when handling events like user input.
Throttling: Ensures that a function executes at most once in a specified time interval. This is useful for limiting the rate at which an event handler executes, such as handling scroll events.
What's the Catch with Using Lodash Debounce & Lodash Throttle in React?
React components often re-render due to state changes, which can lead to unexpected behavior when using utility functions like debounce
and throttle
from Lodash. Let's take a look at a common mistake when using debounce
in a React component:
import { useState } from 'react';
import debounce from 'lodash.debounce';
const SomeChildComponent = ({}) => {
const debouncedFunction = debounce(() => {
console.log('Debounced function called');
}, 500);
return (
<button onClick={debouncedFunction}>Click me</button>
);
};
Why is This Incorrect?
The issue here is that the debouncedFunction
is re-created on every render. This means that the debouncing effect is lost because a new instance of the debounced function is generated each time the component re-renders. As a result, the function won't behave as expected, and the debounce logic will fail.
How to Fix It?
To ensure that the debounced function only has one instance across re-renders and retains its behavior, we need to memoize it using the useCallback
hook. This way, the function is only created once and reused on subsequent renders:
import { useCallback } from 'react';
import debounce from 'lodash.debounce';
const SomeChildComponent = (props) => {
const debouncedFunction = useCallback(
debounce(() => {
console.log('Debounced function called');
}, 500),
[]
);
return (
<button onClick={debouncedFunction}>Click me</button>
);
};
The above code works as expected, but what if the debounced function depends on some state or other dependencies? Let’s explore a common mistake when using a memoized debounce
with useCallback
in a React component:
const {count, setCount} = props;
const debouncedIncrement = useCallback(
debounce(() => {
setCount(count + 1);
}, 300),
[count, setCount]
);
// Usage inside an event handler
<button onClick={() => debouncedIncrement()}>
Click me
</button>
Why is This Incorrect?
The issue here is that the debouncedIncrement
function is re-created every time the count
state changes. This happens because count
is listed as a dependency in the useCallback
hook. As a result, the debounce effect is lost whenever count
changes, and the function no longer behaves as intended. Same goes for setCount
function dependency of debouncedIncrement
function as well.
How to Fix It?
To fix this, we need to ensure that the debouncedIncrement
function is pure and doesn’t rely on external dependencies. Instead, we can pass the necessary external dependecies like the count
state and setCount
setter function as arguments to the debounced function. Here’s how you can do it:
const {count, setCount} = props;
const debouncedIncrement = useCallback(
debounce(({ count, setCount }) => {
setCount(count + 1);
}, 300),
[]
);
// Usage inside an event handler
<button onClick={() => debouncedIncrement({ count, setCount })}>
Click me
</button>
This approach ensures that the debouncedIncrement
function is only created once and doesn’t lose its debouncing effect, even when count
or setCount
change.
Conclusion
So, to sum up, when using debounce
or throttle
in React, make sure to memoize the function using useCallback
to avoid losing its intended behavior. If the debounced function depends on external state or props, pass them as arguments to the debounced function instead of listing them as dependencies in useCallback
. Always make sure that the debounced function is pure and doesn’t rely on external dependencies to maintain its debouncing effect. Instead, pass the external dependencies as arguments to the debounced function
We can create a custom hooks for convenience:
import debounce from 'lodash.debounce'
import throttle from 'lodash.throttle'
import { useCallback } from 'react'
export function useDebouncedCallback(pureFunc, wait, options) {
return useCallback(
debounce(pureFunc, wait, options),
[]
)
}
export function useThrottledCallback(pureFunc, wait, options) {
return useCallback(
throttle(pureFunc, wait, options),
[]
)
}
For TypeScript users, you can checkout this GitHub Gist for the hook definitions with proper types. Give it a star if you find it useful!
Subscribe to my newsletter
Read articles from Ashutosh Khanduala directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ashutosh Khanduala
Ashutosh Khanduala
I am a Android/Web developer. I use React Native, Next.js and TypeScript to make cool stuff. Checkout my GitHub: https://github.com/ashuvssut