Using throttle and debounce in a React function component
You have your shiny function component with a callback that needs to be debounced. Say, to fire it only when the user has finished typing. Something like this:
import React, {useState} from "react";
import _ from "lodash";const sendQuery = (query) => console.log(`Querying for ${query}`);const Search = () => {
const [userQuery, setUserQuery] = useState("");
const delayedQuery = _.debounce(q => sendQuery(q), 500);
const onChange = e => {
setUserQuery(e.target.value);
delayedQuery(e.target.value);
};
return (
<div>
<label>Search:</label>
<input onChange={onChange} value={userQuery} />
</div>
);
}
It won’t work. delayedQuery gets called after 500ms for every keypress.
This is a caveat of function components. Local variables inside a function expires after every call. Every time the component is re-evaluated, the local variables gets initialized again.
Throttle and debounce works using window.setTimeout() behind the scenes. Every time the function component is evaluated, you are registering a fresh setTimeout callback.
You have to store a reference to the debounced callback somehow. Unfortunately, useState() will not work for functions.
useRef() to the rescue
In a function component or a custom hook, useRef() is the equivalent to member variables . Value returned by useRef() does not get re-evaluated every time the functional component is executed. The only inconvenience is that you have to access your stored value via the .current property. So your component will now look like this:
const SearchFixed = () => {
const [userQuery, setUserQuery] = useState("");
const delayedQuery = useRef(_.debounce(q => sendQuery(q), 500)).current;
const onChange = e => {
setUserQuery(e.target.value);
delayedQuery(e.target.value);
};
return (
<div>
<label>Search Fixed:</label>
<input onChange={onChange} value={userQuery} />
</div>
);
};
You can try this here: https://codesandbox.io/s/functional-component-debounce-5jtfd
A better solution: useCallback()
Edit: Thanks to Vlad Safonichev for mentioning in the comments that you can also use useCallback() to solve this. I think this is probably a better semantically correct solution than using useRef().
const delayedQuery = useCallback(_.debounce(q => sendQuery(q), 500), []);
CodeSandBox here.
Functional components are awesome. Did you know that by abandoning class components you can improve your developer experience tremendously? Here is how: