Using Lodash Debounce with React Hooks for an Async Data Fetching Input or use a Custom Hook.
Alexandre Desroches
Posted on August 17, 2021
TLDR; Link to code example that integrates Lodash Debounce within a React function component:
https://codesandbox.io/s/react-debounced-data-fetching-input-630jk?file=/pages/index.js
Link to example code with useDebounce custom hook (no lodash dependency - Thanks to jackzhoumine for posting this idea in the comments):
https://codesandbox.io/s/react-debounced-data-fetching-input-630jk?file=/pages/with-use-debounce-custom-hook.js
An Autocomplete input with React - it was supposed to be simple.
I recently applied for a React Developer job at a big gaming company. They required me to pass an online coding challenge which was to build an Autocomplete component in React.
The requirements were something like that:
- Fetch data on a server to get matches with the user's input.
- Delay the fetching function by 500 ms after user has stopped typing with Lodash Debounce.
- Render a Suggestions List component when there are matches with the user input.
Surely, an autocomplete is not the easiest task, but I never thought the hardest part would be using Lodash's debounce.
Well, it was much more complex than I expected...
It turns out that after 1 full hour, I still could not get the Lodash's Debounce part to work within my React component. Sadly, my maximum allowed time expired and my challenge failed.
Perfect opportunity to improve with React's mental model.
Rather than feeling bad because of a sense of failure, I took that motivation to read about "How to use Lodash debounce with React Hooks", and then I made a CodesandBox to share what I learned.
1. Using useMemo to return the Debounced Change Handler
You can't just use lodash.debounce and expect it to work. It requires useMemo or useCallback to keep the function definition intact between rerenders.
Once you know that, it seems easy.
import { useEffect, useMemo, useState } from "react";
import debounce from "lodash/debounce";
// References:
// https://dmitripavlutin.com/react-throttle-debounce/
// https://stackoverflow.com/questions/36294134/lodash-debounce-with-react-input
// https://stackoverflow.com/questions/48046061/using-lodash-debounce-in-react-to-prevent-requesting-data-as-long-as-the-user-is
// https://kyleshevlin.com/debounce-and-throttle-callbacks-with-react-hooks
// Sandbox Link:
// https://codesandbox.io/s/react-debounced-data-fetching-input-630jk?file=/pages/index.js
const API_ENDPOINT = "https://jsonplaceholder.typicode.com/todos/1";
const DEBOUNCE_DELAY = 1500;
export default function Home() {
const [queryResults, setQueryResults] = useState(null);
const [isDebounced, setIsDebounced] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const debouncedChangeHandler = useMemo(
() => debounce((userInput) => fetchQuery(userInput), DEBOUNCE_DELAY),
[]
);
// Stop the invocation of the debounced function after unmounting
useEffect(() => {
return () => {
debouncedChangeHandler.cancel();
};
}, [debouncedChangeHandler]);
function handleUserInputChange(event) {
const userInput = event.target.value;
debouncedChangeHandler(userInput);
setIsDebounced(true);
}
function fetchQuery() {
setIsDebounced(false);
setIsLoading(true);
fetch(API_ENDPOINT)
.then((res) => res.json())
.then((json) => {
setQueryResults(json);
setIsLoading(false);
})
.catch((err) => {
setError(err);
setIsLoading(false);
});
}
const DisplayResponse = () => {
if (isDebounced) {
return <p>fetchQuery() is debounced for {DEBOUNCE_DELAY}ms</p>;
} else if (isLoading) {
return <p>Loading...</p>;
} else if (error) {
return <pre style={{ color: "red" }}>{error.toString()}</pre>;
} else if (queryResults) {
return (
<pre>
Server response:
<br />
{JSON.stringify(queryResults)}
</pre>
);
}
return null;
};
return (
<main>
<h1>
With <em>Lodash</em> Debounce
</h1>
<a href="/with-use-debounce-custom-hook">
Try with useDebounce custom hook instead
</a>
<div className="input-container">
<label htmlFor="userInput">Type here:</label>
<input
type="text"
id="userInput"
autoComplete="off"
placeholder={"input is delayed by " + DEBOUNCE_DELAY}
onChange={handleUserInputChange}
/>
</div>
<DisplayResponse />
</main>
);
}
For the full code example of using Lodash's Debounce with a React function component, please try the Codesandbox dev environnement which I built upon a Next JS starter template at this URL:
https://codesandbox.io/s/react-debounced-data-fetching-input-630jk?file=/pages/index.js
2. Use a Custom React Hook to debounce fetching
import { useEffect, useState } from "react";
// References:
// https://dev.to/jackzhoumine/comment/1h9c8
// CodesandBox link:
// https://codesandbox.io/s/react-debounced-data-fetching-input-630jk?file=/pages/with-use-debounce-custom-hook.js
const API_ENDPOINT = "https://jsonplaceholder.typicode.com/todos/1";
const DEBOUNCE_DELAY = 1500;
export default function DebouncedInput() {
const [queryResults, setQueryResults] = useState(null);
const [isDebounced, setIsDebounced] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [didMount, setDidMount] = useState(false);
const [userInput, setUserInput] = useState(null);
const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY);
useEffect(() => {
if (!didMount) {
// required to not call API on initial render
//https://stackoverflow.com/questions/53179075/with-useeffect-how-can-i-skip-applying-an-effect-upon-the-initial-render
setDidMount(true);
return;
}
fetchQuery(debouncedUserInput);
}, [debouncedUserInput]);
function handleUserInputChange(event) {
setUserInput(event.target.value);
setIsDebounced(true);
}
function fetchQuery(debouncedUserInput) {
setIsLoading(true);
setIsDebounced(false);
console.log("debouncedUserInput: " + debouncedUserInput);
fetch(API_ENDPOINT)
.then((res) => res.json())
.then((json) => {
setQueryResults(json);
setIsLoading(false);
})
.catch((err) => {
setError(err);
setIsLoading(false);
});
}
const DisplayResponse = () => {
if (isDebounced) {
return <p>fetchQuery() is debounced for {DEBOUNCE_DELAY}ms</p>;
} else if (isLoading) {
return <p>Loading...</p>;
} else if (error) {
return <pre style={{ color: "red" }}>{error.toString()}</pre>;
} else if (queryResults) {
return (
<pre>
Server response:
<br />
{JSON.stringify(queryResults)}
</pre>
);
}
return null;
};
return (
<main>
<h1>
With <em>useDebounce</em> custom hook
</h1>
<a href="/">Try with Lodash Debounce instead</a>
<div className="input-container">
<label htmlFor="userInput">Type here:</label>
<input
type="text"
id="userInput"
autoComplete="off"
placeholder={"input is delayed by " + DEBOUNCE_DELAY}
onChange={handleUserInputChange}
/>
</div>
<DisplayResponse />
</main>
);
}
function useDebounce(value, wait = 500) {
const [debounceValue, setDebounceValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebounceValue(value);
}, wait);
return () => clearTimeout(timer); // cleanup when unmounted
}, [value, wait]);
return debounceValue;
}
For the full code example of using useDebounce Custom React Hook, please try the Codesandbox dev environnement which I built upon a Next JS starter template at this URL:
https://codesandbox.io/s/react-debounced-data-fetching-input-630jk?file=/pages/with-use-debounce-custom-hook.js
Credits:
Credits all go to other smarter people which I referenced in the file's comments. These are more complete articles which will be able to give you a better perspective about the challenge.
That said, I feel like sleeping after all this. But as always, learning with real challenges is best. Keep up the good work. Cheers.
Alex
Posted on August 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 28, 2024