useDebounce
The useDebounce hook delays updating a value until after a specified amount of time has passed since the last change. This is useful for optimizing performance in scenarios like search inputs, where you want to wait for the user to stop typing before making an API call.
Installation
npm install @craft-ui/hooks
import { useDebounce } from "@craft-ui/hooks";
import { useDebounce } from "@craft-ui/hooks";
import { useState } from "react";
function SearchInput() {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// This effect will only run when the user stops typing for 500ms
useEffect(() => {
if (debouncedSearchTerm) {
// Make API call
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
Parameters
The value to debounce. Can be of any type.
The delay in milliseconds before the value is updated.
Returns
The debounced value that updates only after the specified delay has passed since the last change to the input value.
Type Definition
function useDebounce<T>(value: T, delay?: number): T;
Examples
Search with API Calls
import { useDebounce } from "@craft-ui/hooks";
import { useState, useEffect } from "react";
function UserSearch() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
setLoading(true);
fetch(`/api/users?q=${debouncedQuery}`)
.then(res => res.json())
.then(data => {
setResults(data);
setLoading(false);
});
} else {
setResults([]);
}
}, [debouncedQuery]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search users..."
/>
{loading && <div>Loading...</div>}
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Form Validation
import { useDebounce } from "@craft-ui/hooks";
import { useState, useEffect } from "react";
function UsernameInput() {
const [username, setUsername] = useState("");
const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
const [checking, setChecking] = useState(false);
const debouncedUsername = useDebounce(username, 500);
useEffect(() => {
if (debouncedUsername.length >= 3) {
setChecking(true);
fetch(`/api/check-username?username=${debouncedUsername}`)
.then(res => res.json())
.then(data => {
setIsAvailable(data.available);
setChecking(false);
});
} else {
setIsAvailable(null);
}
}, [debouncedUsername]);
return (
<div>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Choose username"
/>
{checking && <span>Checking...</span>}
{isAvailable === true && <span>✓ Available</span>}
{isAvailable === false && <span>✗ Not available</span>}
</div>
);
}
Auto-save Feature
import { useDebounce } from "@craft-ui/hooks";
import { useState, useEffect } from "react";
function NoteEditor() {
const [content, setContent] = useState("");
const [saveStatus, setSaveStatus] = useState<"saved" | "saving" | "unsaved">("saved");
const debouncedContent = useDebounce(content, 1000);
useEffect(() => {
if (debouncedContent && saveStatus === "unsaved") {
setSaveStatus("saving");
fetch("/api/save-note", {
method: "POST",
body: JSON.stringify({ content: debouncedContent }),
})
.then(() => setSaveStatus("saved"));
}
}, [debouncedContent]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value);
setSaveStatus("unsaved");
};
return (
<div>
<textarea
value={content}
onChange={handleChange}
placeholder="Start typing..."
/>
<div>Status: {saveStatus}</div>
</div>
);
}
Window Resize Handler
import { useDebounce } from "@craft-ui/hooks";
import { useState, useEffect } from "react";
function ResponsiveComponent() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const debouncedWidth = useDebounce(windowWidth, 200);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
// This only runs after resize stops for 200ms
console.log("Window resized to:", debouncedWidth);
// Perform expensive calculations here
}, [debouncedWidth]);
return <div>Window width: {debouncedWidth}px</div>;
}
Filter with Multiple Criteria
import { useDebounce } from "@craft-ui/hooks";
import { useState, useEffect } from "react";
function ProductFilter() {
const [filters, setFilters] = useState({
name: "",
minPrice: 0,
maxPrice: 1000,
});
const debouncedFilters = useDebounce(filters, 500);
useEffect(() => {
// Fetch filtered products
fetch(`/api/products?${new URLSearchParams(debouncedFilters)}`)
.then(res => res.json())
.then(data => console.log(data));
}, [debouncedFilters]);
return (
<div>
<input
value={filters.name}
onChange={(e) => setFilters(f => ({ ...f, name: e.target.value }))}
placeholder="Product name"
/>
<input
type="number"
value={filters.minPrice}
onChange={(e) => setFilters(f => ({ ...f, minPrice: Number(e.target.value) }))}
/>
<input
type="number"
value={filters.maxPrice}
onChange={(e) => setFilters(f => ({ ...f, maxPrice: Number(e.target.value) }))}
/>
</div>
);
}
Common Patterns
Custom Delay Based on Input Length
function SmartSearch() {
const [query, setQuery] = useState("");
const delay = query.length < 3 ? 1000 : 300; // Longer delay for short queries
const debouncedQuery = useDebounce(query, delay);
// ...
}
Combining with Loading State
function SearchWithLoader() {
const [term, setTerm] = useState("");
const debouncedTerm = useDebounce(term, 500);
const isSearching = term !== debouncedTerm;
return (
<div>
<input value={term} onChange={e => setTerm(e.target.value)} />
{isSearching && <Spinner />}
</div>
);
}
- The debounced value will be the same as the input value on the initial render
- The timer is reset every time the input value changes
- The cleanup function ensures the timeout is cleared when the component unmounts or when the value changes
- This hook is useful for reducing API calls, optimizing re-renders, and improving performance
- Consider using a shorter delay (200-300ms) for better user experience in search interfaces
- For very expensive operations, a longer delay (500-1000ms) may be appropriate