React useDebounce Hook
A reusable React hook for debouncing values, with TypeScript types and usage example.
import { useState, useEffect } from "react";
/**
* Debounce a value by a given delay.
*
* Returns the debounced value which only updates
* after the specified delay has elapsed without changes.
*/
export function useDebounce<T>(value: T, delay: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
import { useState, useEffect } from "react";
import { useDebounce } from "./useDebounce";
interface SearchResult {
id: number;
title: string;
excerpt: string;
}
interface SearchInputProps {
endpoint: string;
placeholder?: string;
onResults?: (results: SearchResult[]) => void;
}
export function SearchInput({
endpoint,
placeholder = "Search...",
onResults,
}: SearchInputProps) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [isLoading, setIsLoading] = useState(false);
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (!debouncedQuery.trim()) {
setResults([]);
onResults?.([]);
return;
}
const controller = new AbortController();
async function search() {
setIsLoading(true);
try {
const url = `${endpoint}?q=${encodeURIComponent(debouncedQuery)}`;
const res = await fetch(url, { signal: controller.signal });
const data: SearchResult[] = await res.json();
setResults(data);
onResults?.(data);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
console.error("Search failed:", err);
} finally {
setIsLoading(false);
}
}
search();
return () => controller.abort();
}, [debouncedQuery, endpoint, onResults]);
return (
<div className="search-input">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
aria-label="Search"
/>
{isLoading && <span className="search-spinner" aria-hidden="true" />}
{results.length > 0 && (
<ul className="search-results" role="listbox">
{results.map((item) => (
<li key={item.id} role="option">
<strong>{item.title}</strong>
<p>{item.excerpt}</p>
</li>
))}
</ul>
)}
</div>
);
}