Debouncing in React
`Debouncing` is the concept of restricting or limiting the number of times a function gets executed. Instead of calling a function at the click of a button or the stroke of a key, debouncing calls a function only after a period of time, which is set using the native JavaScript method: `setTimeout`.
Source Code
useDebounce
jamesdrury
Why Debounce?
There are a few reasons you may want to debounce. Let’s say, for example, your database is queried every time a user enters a keystroke in a search input. This will cause a detrimental amount of fetches to be made to your database.
This is where debouncing comes in. With debouncing, you can create a delay between when a keystroke is entered and a data fetch is made. If during that delay, the user enters another keystroke, the timer will reset and the waiting period will restart. As long as the user keeps typing before the waiting period ends, the timer will keep resetting, and a fetch will not be made. Only after a user is done typing for the entire time of the delay, will it query the database.
Let’s see how to implement this in React:
const [text, textSet] = React.useState("");
const [debounced, debouncedSet] = React.useState("");
React.useEffect(() => {
const timeout = setTimeout(() => debouncedSet(text), 500);
return () => clearTimeout(timeout);
}, [text]);
In the code above, `useEffect` takes in text as a dependency. This means every time the state of text changes, useEffect will trigger and create a timeout that sets `debounced` to the value of text after 500 milliseconds. If the state of text changes before 500 milliseconds has passed, `clearTimeout` will run, so that when useEffect re-triggers, it creates a new timeout, which will then try to set debounced to the new value of text after 500 milliseconds. As long as `text` keeps changing before 500 milliseconds has passed, the value of debounce will not update. Only when the set time has passed, will debounce update to the new text value.
Let's turn this logic into a custom hook, so we can re-use it throughout our application:
//useDebounce.ts
export default function useDebounce(text: string) {
const [debounced, debouncedSet] = React.useState(text);
React.useEffect(() => {
const timeout = setTimeout(() => debouncedSet(text), 500);
return () => clearTimeout(timeout);
}, [text]);
return debounced;
}
//page.tsx
const [text, textSet] = React.useState("");
const debounce = useDebounce(text)
Refactoring useDebounce
We can now call this hook from anywhere in our application. Currently, the delay time is hardcoded to 500 milliseconds. Let's make this hook more flexible, by passing the milliseconds as a parameter:
//useDebounce.ts
export default function useDebounce(text: string, ms: number) {
const [debounced, debouncedSet] = React.useState(text);
React.useEffect(() => {
const timeout = setTimeout(() => debouncedSet(text), ms);
return () => clearTimeout(timeout);
}, [text, ms]);
return debounced;
}
//page.tsx
const [text, textSet] = React.useState("");
const debounce = useDebounce(text, 500)
We can now debounce simply by defining a text and the amount of time we want to delay that text. But, let's say instead of passing text, we want to pass a number. Perhaps, we want to call the database by clicking a counter button that increments. We can achieve this in our hook by using `Typescript Generics`:
export default function useDebounce<T>(text: T, ms: number) {
const [debounced, debouncedSet] = React.useState<T>(text);
React.useEffect(() => {
const timeout = setTimeout(() => debouncedSet(text), ms);
return () => clearTimeout(timeout);
}, [text, ms]);
return debounced;
}
Now, we can pass any data type to the text parameter. And `useState` will know what type of data to update, given that it has access to type T inside the hook.
Third-Party Packages
Finally, if you don't want to write your own custom hook for debouncing, here are a few different packages you can install that provide debouncing out-of-the-box:
use-debounce
npm i use-debounce
import { useDebounce } from "use-debounce"
const [text, textSet] = React.useState("");
const [debounced] = useDebounce(text, 500)
lodash
npm i lodash
npm i -D @types/lodash
without lodash:
const [text, textSet] = React.useState("");
const debounced = useDebounce(text, 500)
<input
type="text"
placeholder="Type Something..."
className="py-1 px-3 mb-4 rounded w-full border-2 border-stone-400 outline-black"
onChange={(e) => textSet(e.target.value)}
/>
with lodash:
import _ from "lodash";
const [text, textSet] = React.useState("");
const onChange = (e: React.ChangeEvent<HTMLInputElement>) =>
textSet(e.target.value);
const debounced = _.debounce(onChange, 500);
<input
type="text"
placeholder="Type Something..."
className="py-1 px-3 mb-4 rounded w-full border-2 border-stone-400 outline-black"
onChange={debounced}
/>