Debouncing with React hooks and lodash
December 28, 2020
What is debouncing?
The easiest way to explain debouncing is to imagine a text field field that triggers a search (for say movies) as you type. We don't want to perform a search for every keystroke the user types in. What we want to do is to wait for the user to finish typing and then perform a search. Debouncing allows us to do that. Debouncing waits for the user to pause for a defined period of time before firing the event.
Debouncing and Throttling are explained visually and in much more detail here.
Usage with React functional components
Any code inside a react functional function gets executed every time it re-renders. This makes it hard to implement a debounce function because it gets re-initialized every time this occurs. With react hooks, it is now possible (and very easy) to add debounce functions inside a functional component.
The useDebounce hook
import { useCallback } from "react";
import { debounce } from "lodash-es";
export const useDebounce = (fnToDebounce, durationInMs = 200) => {
if (isNaN(durationInMs)) {
throw new TypeError("durationInMs for debounce should be a number");
}
if (fnToDebounce == null) {
throw new TypeError("fnToDebounce cannot be null");
}
if (typeof fnToDebounce !== "function") {
throw new TypeError("fnToDebounce should be a function");
}
return useCallback(debounce(fnToDebounce, durationInMs), [fnToDebounce, durationInMs]);
};
export default useDebounce;
Looking at the code, 9/10 lines validate the inputs passed into the hook, only 1 line of code is actually needed to get the debounce working.
How to use useDebounce?
Using this in a react component is very straightforward. Let's look at the code sample below.
import React, { useState } from "react";
import useDebounce from "./useDebounce";
export const MyComponent = () => {
const [displayText, setDisplayText] = useState("");
const debouncedFunction = useDebounce(value => {
// Do anything here. For this example, i'm just setting the state
setDisplayText(value);
}, 300);
return (
<>
<h2>useDebounce example</h2>
<input onChange={ev => debouncedFunction(ev.currentTarget.value)} />
<pre>Current Value: {displayText}</pre>
</>
);
};
How this works
The magic behind useDebounce lies in the useCallback{:target="_blank"} hook. The useCallback hook is a core hook of React. This hook return a memoized function that does not change unless one of it's dependencies ( passed into it as the second argument) change.
From our perspective, that means that the function returned from useCallback will not be re-declared unless either the fnToDebounce changes or the durationInMs changes.
By using the useCallback hook, we have created a function that is declared once and that is not recreated whenever a re-render of the component occurs.
Extending this further
A common use case for the useDebounce hook is to delay the update to state. The example above does achieve this, but react hooks allows us to build a more graceful solution.
import { useState } from "react";
import useDebounce from "./useDebounce";
export const useDebouncedState = (initialState, durationInMs = 200) => {
const [internalState, setInternalState] = useState(initialState);
const debouncedFunction = useDebounce(setInternalState, durationInMs);
return [internalState, debouncedFunction];
};
export default useDebouncedState;
The useDebouncedState function wraps the useState hook and the useDebounce hook together. This hook has similar interface to the useState hook. It accepts an initial state and an optional second argument that indicates the debounce interval.
Using useDebouncedState
Using this new hook is as simple as changing useState declarations with useDebouncedState. Everything else works as is.
import React from "react";
import useDebouncedState from "./useDebouncedState";
function App() {
const [displayText, setDisplayText] = useDebouncedState("");
return (
<>
<h2>useDebounce example</h2>
<input onChange={ev => setDisplayText(ev.currentTarget.value)} />
<pre>Current Value: {displayText}</pre>
</>
);
}