@usefy/use-debounce-callback
v0.2.4
Published
A React hook for debouncing callback functions
Maintainers
Readme
Overview
@usefy/use-debounce-callback provides a debounced version of your callback function with full control methods: cancel(), flush(), and pending(). Perfect for API calls, form submissions, event handlers, and any scenario requiring debounced function execution with fine-grained control.
Part of the @usefy ecosystem — a collection of production-ready React hooks designed for modern applications.
Why use-debounce-callback?
- Zero Dependencies — Pure React implementation with no external dependencies
- TypeScript First — Full type safety with generics and exported interfaces
- Full Control —
cancel(),flush(), andpending()methods - Flexible Options — Leading edge, trailing edge, and maxWait support
- SSR Compatible — Works seamlessly with Next.js, Remix, and other SSR frameworks
- Lightweight — Minimal bundle footprint (~500B minified + gzipped)
- Well Tested — Comprehensive test coverage with Vitest
Installation
# npm
npm install @usefy/use-debounce-callback
# yarn
yarn add @usefy/use-debounce-callback
# pnpm
pnpm add @usefy/use-debounce-callbackPeer Dependencies
This package requires React 18 or 19:
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}Quick Start
import { useDebounceCallback } from "@usefy/use-debounce-callback";
function SearchInput() {
const [query, setQuery] = useState("");
const debouncedSearch = useDebounceCallback((searchTerm: string) => {
fetchSearchResults(searchTerm);
}, 300);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
return (
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Search..."
/>
);
}API Reference
useDebounceCallback<T>(callback, delay?, options?)
A hook that returns a debounced version of the provided callback function.
Parameters
| Parameter | Type | Default | Description |
| ---------- | ----------------------------------- | ------- | ---------------------------------- |
| callback | T extends (...args: any[]) => any | — | The callback function to debounce |
| delay | number | 500 | The debounce delay in milliseconds |
| options | UseDebounceCallbackOptions | {} | Additional configuration options |
Options
| Option | Type | Default | Description |
| ---------- | --------- | ------- | ---------------------------------------------- |
| leading | boolean | false | Invoke on the leading edge (first call) |
| trailing | boolean | true | Invoke on the trailing edge (after delay) |
| maxWait | number | — | Maximum time to wait before forcing invocation |
Returns DebouncedFunction<T>
| Property | Type | Description |
| ----------- | --------------- | --------------------------------------------------- |
| (...args) | ReturnType<T> | The debounced function (same signature as original) |
| cancel | () => void | Cancels any pending invocation |
| flush | () => void | Immediately invokes any pending invocation |
| pending | () => boolean | Returns true if there's a pending invocation |
Examples
Auto-Save with Cancel
import { useDebounceCallback } from "@usefy/use-debounce-callback";
function Editor() {
const [content, setContent] = useState("");
const debouncedSave = useDebounceCallback((text: string) => {
saveToServer(text);
console.log("Auto-saved");
}, 1000);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value);
debouncedSave(e.target.value);
};
const handleManualSave = () => {
// Flush any pending save immediately
debouncedSave.flush();
};
const handleDiscard = () => {
// Cancel pending save and reset content
debouncedSave.cancel();
setContent("");
};
return (
<div>
<textarea value={content} onChange={handleChange} />
<button onClick={handleManualSave}>Save Now</button>
<button onClick={handleDiscard}>Discard</button>
{debouncedSave.pending() && <span>Saving...</span>}
</div>
);
}Search with Immediate First Call
import { useDebounceCallback } from "@usefy/use-debounce-callback";
function SearchWithSuggestions() {
const [results, setResults] = useState([]);
// First keystroke triggers immediate search, then debounce
const debouncedSearch = useDebounceCallback(
async (query: string) => {
const data = await fetch(`/api/search?q=${query}`);
setResults(await data.json());
},
300,
{ leading: true }
);
return (
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
);
}Form Validation
import { useDebounceCallback } from "@usefy/use-debounce-callback";
function RegistrationForm() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const validateEmail = useDebounceCallback(async (value: string) => {
if (!value.includes("@")) {
setError("Invalid email format");
return;
}
const response = await fetch(`/api/check-email?e=${value}`);
const { available } = await response.json();
setError(available ? "" : "Email already registered");
}, 500);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
setError(""); // Clear error immediately
validateEmail(e.target.value);
};
return (
<div>
<input
type="email"
value={email}
onChange={handleChange}
placeholder="Enter email"
/>
{error && <span className="error">{error}</span>}
</div>
);
}Event Handler with maxWait
import { useDebounceCallback } from "@usefy/use-debounce-callback";
function ResizeHandler() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
// Debounce resize events, but guarantee update every 1 second
const handleResize = useDebounceCallback(
() => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
},
250,
{ maxWait: 1000 }
);
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
handleResize.cancel();
window.removeEventListener("resize", handleResize);
};
}, [handleResize]);
return (
<div>
Window: {dimensions.width} x {dimensions.height}
</div>
);
}API Request with Pending State
import { useDebounceCallback } from "@usefy/use-debounce-callback";
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const fetchData = useDebounceCallback(async (params: QueryParams) => {
setLoading(true);
try {
const response = await fetch("/api/data", {
method: "POST",
body: JSON.stringify(params),
});
setData(await response.json());
} finally {
setLoading(false);
}
}, 500);
return (
<div>
<button onClick={() => fetchData({ page: 1 })}>
{fetchData.pending() ? "Request pending..." : "Fetch Data"}
</button>
{loading && <Spinner />}
</div>
);
}Cleanup on Unmount
import { useDebounceCallback } from "@usefy/use-debounce-callback";
function Component() {
const debouncedAction = useDebounceCallback(() => {
// Some action
}, 500);
// Cancel pending on unmount
useEffect(() => {
return () => {
debouncedAction.cancel();
};
}, [debouncedAction]);
return <button onClick={debouncedAction}>Action</button>;
}TypeScript
This hook is written in TypeScript with full generic support.
import {
useDebounceCallback,
type UseDebounceCallbackOptions,
type DebouncedFunction,
} from "@usefy/use-debounce-callback";
// Type inference from callback
const debouncedFn = useDebounceCallback((a: string, b: number) => {
return `${a}-${b}`;
}, 300);
// debouncedFn(string, number) => string | undefined
// debouncedFn.cancel() => void
// debouncedFn.flush() => void
// debouncedFn.pending() => booleanTesting
This package maintains comprehensive test coverage to ensure reliability and stability.
Test Coverage
📊 View Detailed Coverage Report (GitHub Pages)
Test Categories
- Cancel pending invocations
- Flush immediately invokes pending callback
- pending() returns correct state
- cancel() clears pending state
- flush() clears pending state after invocation
- Invoke on leading edge with leading: true
- No immediate invoke with leading: false (default)
- Invoke on trailing edge with trailing: true (default)
- No trailing invoke with trailing: false
- Combined leading and trailing options
License
MIT © mirunamu
This package is part of the usefy monorepo.
