Built with Next.js and Chakra UI, deployed with Vercel. All text is set in the Inter typeface.
Copyright 2025 | prasetya_webspace-v2.0.8
Contributor
Prasetya Ikra Priyadi
[email protected]In some cases, I need to ensure users receive the right content based on their queries. This is typically done by allowing them to fetch data via API calls. However, when users rapidly change their queries, it can lead to multiple API requests in a short time, which may slow down performance and increase server load.
To maintain a smooth user experience while keeping API calls efficient, we can use a debounce function. This technique delays the execution of a function until the user stops typing or interacting for a specified time. By implementing debouncing in React, we can prevent unnecessary API calls, reduce re-renders, and optimize overall app performance.
The term debounce comes from electronics, where it is used to prevent unintended multiple signals caused by mechanical button presses. When you press a button, such as on a TV remote, the physical contact inside the switch doesn’t settle instantly. Instead, it rapidly oscillates (or "bounces") for a few milliseconds before stabilizing, causing the microchip to register multiple clicks instead of one.
To prevent this, debouncing introduces a short delay after the first signal is detected. During this delay, the microchip ignores additional signals from the button, ensuring that only a single input is processed, even if the button physically bounces. This same concept is applied in software to control the frequency of function execution, such as preventing excessive API calls in React applications.
To implement a debounce function in javascript, we can define a function like this
export function debounce<T extends (...args: never[]) => void>(
callback: T,
timeout = 300
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => {
callback(...args);
}, timeout);
};
}
The provided debounce function takes two parameters: a callback
function and an optional timeout
(in my cases, the default is 300 miliseconds, but its up to you). Inside the function, a variable timer
is declared to store a reference to the setTimeout
function. When the returned function is called, it first clears any existing timeout using clearTimeout(timer)
, ensuring that any previously scheduled execution of callback
is canceled. Then, a new timeout is set using setTimeout
, which will execute the callback
function after the specified timeout
period. This process effectively resets the timer each time the function is called, preventing the callback
from executing too frequently. Only when no further calls occur within the timeout
duration does the function finally execute. This approach is useful in scenarios such as handling user input, where it ensures that expensive operations (e.g., API calls) are only triggered after the user has finished typing, improving performance and efficiency.
We know that debounce helps prevent multiple events from being triggered repeatedly in a short period. This is especially useful when a user needs to call an API with a changing query. I often encounter this scenario when implementing an auto-complete search, where the user types, and a list of matching data is displayed dynamically
This example allow user to search an article based on title and description that user needs. They simply type the keyword on search input box and the app will automatically calling the API to get the list of Article based on those query.
import React, { useState, useEffect } from "react";
interface Article {
id: number;
title: string;
}
export default function ArticleList() {
const [articles, setArticles] = useState<Article[]>([]);
const [search, setSearch] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
async function fetchArticles(query: string = ""): Promise<void> {
setLoading(true);
try {
const res = await fetch(`/api/articles?search=${query}`);
const data: Article[] = await res.json();
setArticles(data);
} catch (error) {
console.error("Error fetching articles:", error);
} finally {
setLoading(false);
}
}
useEffect(() => {
fetchArticles(search);
}, [search]);
return (
<div>
<input
type="text"
placeholder="Search articles..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{loading && <p>Loading...</p>}
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</div>
);
}
With this code, the search feature functions as expected. When a user types in the search input box, it updates the search
state, which in turn triggers the useEffect
hook to call fetchArticles
, retrieving a new list of articles based on the updated search query.
However, there is an inefficiency in how the API is being called. Every single character input triggers a state change, which immediately triggers a new API request. For example, if a user types "hello", the API is called five times with queries: "h"
, "he"
, "hel"
, "hell"
, and finally "hello"
. This results in unnecessary API calls, increasing server load and potentially slowing down the application.
Ideally, we want to delay the API request until the user has finished typing their search query. This ensures that we only fetch relevant results based on the completed input rather than sending redundant requests. This is where the debounce function comes to the rescue.
import React, { useState, useEffect } from "react";
interface Article {
id: number;
title: string;
}
export default function ArticleList() {
const [articles, setArticles] = useState<Article[]>([]);
const [search, setSearch] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
async function fetchArticles(query: string = ""): Promise<void> {
setLoading(true);
try {
const res = await fetch(`/api/articles?search=${query}`);
const data: Article[] = await res.json();
setArticles(data);
} catch (error) {
console.error("Error fetching articles:", error);
} finally {
setLoading(false);
}
}
// Debounced version of fetchArticles
const debouncedFetchArticles = useCallback(debounce(fetchArticles, 800), []);
useEffect(() => {
debouncedFetchArticles(search);
}, [search, debouncedFetchArticles]);
return (
<div>
<input
type="text"
placeholder="Search articles..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{loading && <p>Loading...</p>}
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</div>
);
}
With this change in our code, we pass the fetchArticles
function as a callback to the debounce
function. Here’s what happens:
When the user starts typing in the search input, the debounce function schedules the callback (fetchArticles
) using setTimeout
, waiting 800 milliseconds before executing it. This ensures the user has at least 800 milliseconds to finish typing before the API call is triggered.
If the user continues typing within this 800-millisecond window, the debounce function cancels the previously scheduled execution and resets the timer with the updated query. This cycle repeats until the user stops typing for at least 800 milliseconds.
Once the delay has passed without any further input, the callback (fetchArticles
) finally executes with the most recent query, ensuring that only the final input is used for the API request. This approach prevents unnecessary API calls and optimizes performance. You can try this solution on my Resources Page to explore it in more detail.
Now you understand how and why to use the debounce function in react—simple and effective, right?
With debouncing, when we type a search query, the results will only update after given time we defined once we stop typing. This prevents unnecessary API calls and improves performance. Debouncing has many useful applications. It helps reduce excessive API requests, ensuring we don’t overload our server