How to Build a Debounced Search Input in React.js
What is Debouncing?
Debouncing is a performance optimization technique. When a user types quickly in a search input, making an API call on every key press is the wrong approach. By using debounce, we only call the API after the user has stopped typing.
"Do not call the API on every key press — call it only after the user stops typing."
Why Debounced Search is Needed?
Suppose a user types:
r re rea reac react
If we make an API call on every character, 5 API calls will be triggered. But by using debounce, only one API call is made on the final search keyword.
React Debounced Search Flow
1. User Types
The input state gets updated.
2. Timer Starts
A setTimeout delay begins.
3. Clear Old Timer
If the user types again, the old timer is cleared.
4. API Call
The API is called only after the user stops typing.
Example 1: Debounced Search Input
import React, { useEffect, useState } from "react";
export default function DebouncedSearch() {
const [searchTerm, setSearchTerm] = useState("");
const [debouncedValue, setDebouncedValue] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(searchTerm);
}, 500);
return () => {
clearTimeout(timer);
};
}, [searchTerm]);
useEffect(() => {
if (!debouncedValue.trim()) {
setResults([]);
return;
}
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/users?name_like=${debouncedValue}`
);
const data = await response.json();
setResults(data);
} catch (error) {
console.error("Search API Error:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, [debouncedValue]);
return (
<div className="mx-auto max-w-xl rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="mb-4 text-2xl font-bold text-gray-900">
Debounced Search Input
</h2>
<input
type="text"
placeholder="Search users..."
value={searchTerm} => setSearchTerm(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 outline-none focus:border-blue-500"
/>
{loading && <p className="mt-4 text-sm text-gray-600">Searching...</p>}
<div className="mt-5 space-y-3">
{results.map((user) => (
<div key={user.id} className="rounded-lg border border-gray-200 p-4">
<h3 className="font-semibold text-gray-900">{user.name}</h3>
<p className="text-sm text-gray-600">{user.email}</p>
</div>
))}
</div>
</div>
);
}
Code Explanation
1. searchTerm State
This state stores the value typed by the user.
2. debouncedValue State
This is the final value that gets updated only after the delay has completed.
3. clearTimeout
If the user types again, the old timer gets cancelled.
Example 2: Custom useDebounce Hook
import React, { useEffect, useState } from "react";
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
export default function SearchWithCustomHook() {
const [query, setQuery] = useState("");
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const debouncedQuery = useDebounce(query, 600);
useEffect(() => {
if (!debouncedQuery.trim()) {
setUsers([]);
return;
}
async function searchUsers() {
try {
setLoading(true);
const res = await fetch(
`https://jsonplaceholder.typicode.com/users?name_like=${debouncedQuery}`
);
const data = await res.json();
setUsers(data);
} catch (error) {
console.log("Error:", error);
} finally {
setLoading(false);
}
}
searchUsers();
}, [debouncedQuery]);
return (
<div className="mx-auto max-w-xl p-6">
<input
value={query} => setQuery(e.target.value)}
placeholder="Type to search..."
className="w-full rounded-lg border border-gray-300 px-4 py-3"
/>
{loading && <p className="mt-3">Loading...</p>}
{users.map((user) => (
<div key={user.id} className="mt-3 rounded-lg border p-3">
<strong>{user.name}</strong>
<p>{user.email}</p>
</div>
))}
</div>
);
}
Best Practices
- Keep the debounce delay between 300ms and 700ms.
- Avoid making an API call when the search query is empty.
- Always show a loading state.
- Always add proper error handling.
- In large applications, creating a custom hook is the better approach.
Interview Answer
In a Debounced Search Input, we optimize API calls by using setTimeout to delay the request. Instead of calling the API on every keystroke, we wait for the user to stop typing. If the user types again within the delay period, clearTimeout cancels the old timer and a new one starts. This reduces unnecessary API calls, improves performance, and reduces server load.