Fragmented Thought

Using debounce onChange handlers with Typescript

By

Published:

Lance Gliser

Heads up! This content is more than six months old. Take some time to verify everything still works as expected.

For this example, I've mixed together lodash's debounce with React's hooks. The same should roughly apply in other frameworks.

For the important bits, we're looking at a few of specifics:

First, note that debounce accepts generic type variables. This provides us with a place to define the return function's type a typical place for typings to be lost working with debounce. Try this instead:

debounce<ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>>(event);

The incoming event is also typically lost, so we can correct that again on the event:

(event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {};

The debounce function above would be minimally functional passed directly to something like onChange. It still suffers two issues we can correct:

There's a minor performance gain to be snagged by caching the debounced function. In React, we can store the results of a function that runs (even if the result is a function), but through useMemo.

const onSearchChange = useMemo( debounce(() => {}, 250), [ // Dependencies ] );

Then there's the potentially nasty side effect of debounce to deal with, timing. Depending on your implementation and side effects, the debounced callback function might try to fire after your component unloads resulting in errors. So, we fire a cancel event using the useEffect's return clean up function.

useEffect(() => { // Note that this is a clean up effect, so its logic must be in a returned function. return () => { onSearchChange?.cancel(); }; }, [onSearchChange]);

For a full working example, have look over this sample component.

import React, { useState, useEffect, ChangeEvent, useMemo, ChangeEventHandler, } from "react"; import { TextField } from "@material-ui/core"; import { debounce } from "lodash"; const SampleComponent: React.FunctionComponent = () => { const [searchQuery, setSearchQuery] = useState<string | undefined>(); // Create a cached debounce function for change handling const onSearchChange = useMemo( () => debounce<ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>>( (event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { setSearchQuery(event.target.value); }, 250 ), [setSearchQuery] ); // Stop the invocation of the debounced function after unmounting useEffect(() => { // Note that this is a clean up effect, so its logic must be in a returned function. return () => { onSearchChange?.cancel(); }; }, [onSearchChange]); return ( <TextField autoFocus placeholder="Page" type={"search"} onChange={onSearchChange} value={searchQuery} /> ); }; export default SampleComponent;