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
Top comments (3)
no need loash actuall.
use this hook
Actually you are right, I managed to make a working example on the same Codesandbox project: codesandbox.io/s/react-debounced-d...
So you can avoid importing Lodash debounce and it works just about the same. The only difference I see is that you have to handle the initial render to NOT trigger the debounced function call. PS: I edited my original post to include this solution.
Thanks for sharing Jackzhoumie. I'll try your solution today. Note: for my test I was required to use Lodash's debounce that's why I had such a strong focus on using it.