JavascriptAdvanced
Debounce Search
Build a search input that waits for the user to stop typing before making an API call using a debounce function.
Debounce Search
Problem
Build a search box that fetches results from an API — but only after the user has stopped typing for 500ms. Without debounce, a network request fires on every single keystroke which is wasteful and slow. With debounce, the request waits until the user pauses.
User types "j" → waiting... (no request yet)
User types "ja" → waiting... (timer resets)
User types "jav" → waiting... (timer resets)
User types "java" → waiting... (timer resets)
User stops typing...
500ms passes → API request fires for "java"
Results appear → shows matching GitHub users
User clears input → results cleared, no requestLogic
- Write a
debouncefunction that wraps another function - The debounce returns a new function that starts a timer on every call
- If called again before the timer ends — reset the timer
- Only when the timer completes — call the original function
- On the search input's
inputevent — call the debounced search function - The search function fetches from GitHub's API
- Render results or show a no results message
Flow
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Debounce Search</title>
<style>
body {
font-family: sans-serif;
max-width: 520px;
margin: 60px auto;
padding: 0 20px;
}
/* search input wrapper */
.search-wrapper {
position: relative;
margin-bottom: 16px;
}
.search-wrapper input {
width: 100%;
padding: 14px 44px 14px 16px;
font-size: 1rem;
border: 2px solid #ccc;
border-radius: 8px;
outline: none;
box-sizing: border-box;
}
.search-wrapper input:focus {
border-color: #555;
}
/* loading spinner inside input */
#spinner {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
border: 2px solid #ddd;
border-top-color: #555;
border-radius: 50%;
animation: spin 0.7s linear infinite;
display: none;
}
@keyframes spin {
to { transform: translateY(-50%) rotate(360deg); }
}
/* status text — shows debounce delay info */
#status {
font-size: 0.85rem;
color: #888;
margin-bottom: 16px;
min-height: 20px;
}
/* results list */
#results {
list-style: none;
padding: 0;
margin: 0;
}
/* each result card */
#results li {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 14px;
border: 1px solid #e0e0e0;
border-radius: 10px;
margin-bottom: 10px;
text-decoration: none;
color: inherit;
transition: background 0.15s;
cursor: pointer;
}
#results li:hover {
background: #f9f9f9;
}
/* avatar image */
#results li img {
width: 44px;
height: 44px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-info .username {
font-weight: 600;
color: #111;
font-size: 1rem;
}
.user-info .profile-link {
font-size: 0.8rem;
color: #888;
}
/* no results */
#no-results {
text-align: center;
color: #aaa;
font-size: 0.95rem;
padding: 24px 0;
display: none;
}
/* error message */
#error {
color: #c0392b;
font-size: 0.9rem;
display: none;
padding: 12px;
background: #fff0f0;
border-radius: 8px;
margin-bottom: 12px;
}
</style>
</head>
<body>
<h1>GitHub User Search</h1>
<p style="color:#888; font-size:0.9rem; margin-bottom: 20px;">
Search waits 500ms after you stop typing before making a request.
</p>
<div class="search-wrapper">
<input
type="text"
id="search-input"
placeholder="Search GitHub users..."
/>
<!-- loading spinner shown during fetch -->
<div id="spinner"></div>
</div>
<!-- shows typing / fetching status -->
<div id="status"></div>
<!-- error message -->
<div id="error"></div>
<!-- results rendered here -->
<ul id="results"></ul>
<!-- no results message -->
<div id="no-results">No users found.</div>
<script src="script.js"></script>
</body>
</html>Solution
// Step 1 — select elements
const searchInput = document.querySelector("#search-input");
const spinner = document.querySelector("#spinner");
const status = document.querySelector("#status");
const results = document.querySelector("#results");
const noResults = document.querySelector("#no-results");
const errorBox = document.querySelector("#error");
// Step 2 — the debounce function
// takes a function and a delay in milliseconds
// returns a new function that delays calling the original
function debounce(fn, delay) {
let timer; // timer lives in the closure — persists between calls
return function (...args) {
// clear the previous timer on every call
// this is what resets the countdown each time the user types
clearTimeout(timer);
// show "typing..." status so user sees the debounce in action
status.textContent = "Typing...";
// start a new timer
// only fires if this function is not called again within delay ms
timer = setTimeout(() => {
fn.apply(this, args); // call the original function with the arguments
}, delay);
};
}
// Step 3 — the actual search function
// this is what gets called after the debounce delay
async function searchUsers(query) {
query = query.trim();
// clear results and error if input is empty
if (!query) {
clearResults();
status.textContent = "";
return;
}
// Step 4 — show loading state
showLoading();
status.textContent = `Searching for "${query}"...`;
try {
// Step 5 — fetch from GitHub search API
// the API searches for users whose username matches the query
const response = await fetch(
`https://api.github.com/search/users?q=${encodeURIComponent(query)}&per_page=6`
);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const data = await response.json();
// Step 6 — render results
hideLoading();
renderResults(data.items, query);
} catch (err) {
hideLoading();
showError(`Failed to fetch results: ${err.message}`);
}
}
// Step 7 — create the debounced version of searchUsers
// 500ms delay — only fires when user stops typing for 500ms
const debouncedSearch = debounce(searchUsers, 500);
// Step 8 — listen for input event and call debounced search
searchInput.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
// Step 9 — render the list of users
function renderResults(users, query) {
results.innerHTML = "";
errorBox.style.display = "none";
if (users.length === 0) {
noResults.style.display = "block";
status.textContent = `No results for "${query}"`;
return;
}
noResults.style.display = "none";
status.textContent = `Found ${users.length} result${users.length !== 1 ? "s" : ""} for "${query}"`;
users.forEach((user) => {
const li = document.createElement("li");
// clicking the result opens the user's GitHub profile
li.addEventListener("click", () => {
window.open(user.html_url, "_blank");
});
li.innerHTML = `
<img src="${user.avatar_url}" alt="${user.login}'s avatar" />
<div class="user-info">
<div class="username">${user.login}</div>
<div class="profile-link">github.com/${user.login}</div>
</div>
`;
results.appendChild(li);
});
}
// helpers
function showLoading() {
spinner.style.display = "block";
results.innerHTML = "";
noResults.style.display = "none";
errorBox.style.display = "none";
}
function hideLoading() {
spinner.style.display = "none";
}
function clearResults() {
results.innerHTML = "";
noResults.style.display = "none";
errorBox.style.display = "none";
}
function showError(message) {
errorBox.textContent = message;
errorBox.style.display = "block";
status.textContent = "";
}Code Execution
Trace through the debounce when user types "java" quickly:
| Keystroke | Action | Timer |
|---|---|---|
"j" | clearTimeout(undefined) → setTimeout(search, 500) | timer 1 starts |
"ja" | clearTimeout(timer1) → setTimeout(search, 500) | timer 1 cancelled, timer 2 starts |
"jav" | clearTimeout(timer2) → setTimeout(search, 500) | timer 2 cancelled, timer 3 starts |
"java" | clearTimeout(timer3) → setTimeout(search, 500) | timer 3 cancelled, timer 4 starts |
| 500ms pass | nothing typed | timer 4 fires → searchUsers("java") |
Trace through API call for "java":
| Step | Code | Result |
|---|---|---|
| Encode query | encodeURIComponent("java") | "java" |
| Fetch URL | https://api.github.com/search/users?q=java&per_page=6 | API call made |
| Check ok | response.ok | true |
| Parse JSON | response.json() | { items: [...6 users] } |
| Render | 6 user cards shown | results visible |
Output
Type "j" → Typing... (no request)
Type "ja" → Typing... (timer resets)
Type "java" → Typing... (timer resets)
Stop typing... → Searching for "java"...
500ms later → Found 6 results for "java"
Clear input → results cleared, no request made