Using Lodash debounce with React and TypeScript
Lodash is a package that contains lots of great utility functions. For example, Lodash’s debounce
function delays invoking a function passed into it. It can help performance in some situations.
In this post, we will use debounce
to search for a Star Wars character when the user stops typing.
An existing app
In our existing app, when the user enters criteria in the input
element, a request is made to the Star Wars API to search for matching characters.
export default function App() {
const [characters, setCharacters] = React.useState<string[]>([]);
async function search(criteria: string) {
const response = await fetch(
`https://swapi.dev/api/people/?search=${criteria}`
);
const body = await response.json();
return body.results.map((result: Character) => result.name);
}
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// 🐌 causes too many requests 😞
setCharacters(await search(e.target.value));
}
return (
<div className="App">
<input
type="search"
placeholder="Enter your search"
onChange={handleChange}
/>
<ul>
{characters.map((character) => (
<li key={character}>{character}</li>
))}
</ul>
</div>
);
}
The problem with the app is that the search request is made every time the user makes a keystroke in the input
element. Ideally, we want the search request to be made only when the user has stopped typing. We can use the debounce
function from Lodash to do this.
Installing Lodash
npm install lodash
Install the whole of Lodash rather than just the function (i.e. npm install lodash.debounce
) because the app’s bundler (e.g. Webpack) will tree shake everything in Lodash that isn’t used in the app.
Lodash doesn’t contain TypeScript types in the core package, so we install these separately:
npm install @types/lodash
Importing debounce from Lodash
debounce
is a named export in the lodash
package, so we import it as follows:
import { debounce } from "lodash"
Using debounce - first attempt
We use debounce
to invoke the call to search
after 300 milliseconds:
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
debounce(async () => {
// 😕 debounced function never called
setCharacters(await search(e.target.value));
}, 300);
}
So, in theory, the search should be invoked 300 milliseconds after the user stops typing.
This implementation doesn’t work though - the search function is never called. 😕
It doesn’t work because the debounced function is lost when the user types the next character, and handleChange
is called again.
Using debounce - second attempt
We can lift the debounced function outside of the handleChange
to resolve the problem:
const debouncedSearch = debounce(async (criteria) => {
setCharacters(await search(criteria));
}, 300);
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
debouncedSearch(e.target.value);
}
This works nicely, but there is a more performant approach.
Using debounce - more performant approach
At the moment, a new version of debouncedSearch
is created on every render. We can use Reacts useRef
to store the debounced function across renders:
const debouncedSearch = React.useRef(
debounce(async (criteria) => {
setCharacters(await search(criteria));
}, 300)
).current;
Nice! 😊
Cleaning up
When the component unmounts, the search request could be still in progress. In this case, an error will occur when the characters
state is set.
We can use the useEffect
hook to cancel the debounced function to resolve this problem. The debounce
handle has a handy cancel
method that we can use to do this:
React.useEffect(() => {
return () => {
debouncedSearch.cancel();
};
}, [debouncedSearch]);
The code from this post is available in Codesandbox in the link below
Did you find this post useful?
Let me know by sharing it on Twitter.If you to learn more about using TypeScript with React, you may find my course useful: