This content originally appeared on DEV Community and was authored by Raj Aryan
Tired of making a dozen API calls every time a user types three letters?
Meet @er-raj-aryan/use-smart-debounce
β a lightweight React hook library that makes debouncing async-safe, TypeScript-ready, and smooth like butter
NPM: @er-raj-aryan/use-smart-debounce
GitHub: https://github.com/er-raj-aryan/use-smart-debounce
Why I Built This
While building a Next.js dashboard, I ran into the same old issue β API calls firing on every keystroke during search input.
Existing solutions like lodash.debounce
or use-debounce
didnβt handle async calls, cancellation, or stale responses well.
So I decided to build something that does:
- Cancels stale API requests
- Handles async promises safely
- Works with
leading
,trailing
, andmaxWait
modes - Comes with full TypeScript support
- Ships with zero dependencies
Installation
npm i @er-raj-aryan/use-smart-debounce
# or
yarn add @er-raj-aryan/use-smart-debounce
Whatβs Inside
Hook | Use Case | Description |
---|---|---|
useDebouncedValue |
Debounce values | Returns a delayed version of any state value |
useDebouncedCallback |
Debounce functions | Debounce any callback with leading/trailing control |
useDebouncedAsync |
Debounce async calls | Cancels in-flight requests & ignores stale responses |
Basic Example β Debounce a Value
import { useDebouncedValue } from "@er-raj-aryan/use-smart-debounce";
import { useState, useEffect } from "react";
export default function SearchBox() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebouncedValue(query, 500);
useEffect(() => {
if (debouncedQuery.length >= 3) {
console.log("Search:", debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to searchβ¦"
/>
);
}
Fires only after 500ms of inactivity
Perfect for live search or filtering
Debounce Callbacks with Control
import { useDebouncedCallback } from "@er-raj-aryan/use-smart-debounce";
function ResizeTracker() {
const debouncedResize = useDebouncedCallback(
() => console.log("Window resized:", window.innerWidth),
300,
{ leading: false, trailing: true }
);
useEffect(() => {
window.addEventListener("resize", debouncedResize);
return () => window.removeEventListener("resize", debouncedResize);
}, []);
return <p>Resize the window to see it in action</p>;
}
Async-Safe Debouncing
This is where most debounce hooks fail β multiple async calls return out of order, and the old response overwrites the new one.
useDebouncedAsync
handles that for you:
import { useDebouncedAsync } from "@er-raj-aryan/use-smart-debounce";
import { useState, useEffect } from "react";
function LiveSearch() {
const [query, setQuery] = useState("");
const { run, status, data, error } = useDebouncedAsync(
async (q: string) => {
if (q.length < 3) return [];
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
const json = await res.json();
return json.results ?? [];
},
500
);
useEffect(() => {
run(query);
}, [query]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{status === "loading" && <p>Loadingβ¦</p>}
{error && <p style={{ color: "red" }}>Error fetching</p>}
<ul>
{Array.isArray(data) &&
data.map((r: any) => <li key={r.id}>{r.name}</li>)}
</ul>
</div>
);
}
Features:
- Cancels the previous API call when user keeps typing
- Prevents stale results from overwriting fresh ones
- Tracks
status
,data
, anderror
Real Example β HS Code Lookup with Material UI
Hereβs a practical example using MUIβs <Autocomplete>
with your HS Code API (/db/hs_code_list/?search_key=
):
import { Autocomplete, TextField } from "@mui/material";
import { useDebouncedAsync } from "@er-raj-aryan/use-smart-debounce";
import { useState, useEffect } from "react";
type HS = { hs_code: string; description: string };
export default function HSCodeSearch() {
const [value, setValue] = useState<HS | null>(null);
const [options, setOptions] = useState<HS[]>([]);
const { run, status, data } = useDebouncedAsync(
async (q: string) => {
if (q.length < 3 || !/^\d+$/.test(q)) return [];
const res = await fetch(`/db/hs_code_list/?search_key=${q}`);
const json = await res.json();
return json.results ?? [];
},
600
);
useEffect(() => {
if (Array.isArray(data)) setOptions(data);
}, [data]);
return (
<Autocomplete
size="small"
options={options}
value={value}
onChange={(_, v) => setValue(v)}
getOptionLabel={(o) => o.hs_code}
onInputChange={(_, v) => run(v)}
renderInput={(params) => (
<TextField
{...params}
label="HS Code"
helperText={
value?.description ||
(status === "loading" ? "Searching..." : "")
}
/>
)}
/>
);
}
Comparison Table
Feature | @er-raj-aryan/use-smart-debounce |
use-debounce |
ahooks |
lodash.debounce |
---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
<3 KB | ~4 KB | ~200 KB | 24 KB |
![]() |
0 | 0 | 20+ | 1 |
![]() |
![]() |
![]() |
![]() |
![]() |
Why Youβll Love It
- No dependencies β ultra-fast build
- Tiny footprint β great for production apps
- Async-safe β ideal for API-driven UIs
- TypeScript-ready β works out of the box
Wrap Up
If youβre building React apps that rely on API queries, form inputs, or live filters β
@er-raj-aryan/use-smart-debounce
will save you time, requests, and user frustration.
Install now:
npm i @er-raj-aryan/use-smart-debounce
Links:
Author: Er Raj Aryan
Frontend Engineer | React / Next.js Developer | Open Source Enthusiast
If you found this useful β the repo or drop a comment!
Letβs build smarter UIs together
This content originally appeared on DEV Community and was authored by Raj Aryan