Skip to main content

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

useDebouncedSearch / useDebounceValue

Two complementary hooks for debouncing values and search operations. useDebounceValue is a low-level primitive that delays value updates, while useDebouncedSearch builds on it to provide a complete search-with-debounce pattern including loading state tracking and duplicate-search prevention.

Import

import { useDebounceSearch } from '@/hooks/use-debounced-search';
import { useDebounceValue } from '@/hooks/use-debounced-value';

API Reference

useDebounceSearch

function useDebounceSearch(props: UseDebounceSearchProps): UseDebounceSearchReturn;

UseDebounceSearchProps

PropertyTypeDefaultDescription
searchValuestringrequiredThe raw search input value (updates on every keystroke).
delaynumber300Debounce delay in milliseconds.
onSearch(value: string) => void | Promise<void>requiredCallback invoked with the debounced search value. Can be async.

UseDebounceSearchReturn

PropertyTypeDescription
debouncedValuestringThe debounced version of searchValue, updated after delay milliseconds of inactivity.
isSearchingbooleantrue while a search is pending or the debounced value has not yet caught up to the input value.
clearSearch() => voidResets the internal search state, clearing the previous-value tracker.

useDebounceValue

function useDebounceValue<T>(value: T, delay?: number): T;
ParameterTypeDefaultDescription
valueTrequiredThe value to debounce.
delaynumber300Debounce delay in milliseconds.

Returns: The debounced value of type T, which updates delay milliseconds after the last change to value.

Usage Examples

Search Input with Loading Indicator

function SearchBar() {
const [query, setQuery] = useState('');

const { debouncedValue, isSearching } = useDebounceSearch({
searchValue: query,
delay: 400,
onSearch: async (value) => {
const results = await fetchSearchResults(value);
setSearchResults(results);
},
});

return (
<div className="relative">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{isSearching && (
<Spinner className="absolute right-3 top-3" />
)}
<p className="text-sm text-gray-500">
{debouncedValue ? `Showing results for "${debouncedValue}"` : 'Type to search'}
</p>
</div>
);
}

Debounced Filter

function FilteredList({ items }: { items: Item[] }) {
const [filterText, setFilterText] = useState('');
const debouncedFilter = useDebounceValue(filterText, 250);

const filteredItems = useMemo(
() => items.filter((item) =>
item.name.toLowerCase().includes(debouncedFilter.toLowerCase())
),
[items, debouncedFilter]
);

return (
<div>
<input
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
placeholder="Filter items..."
/>
{filteredItems.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}

Search with URL Sync

function SearchPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [query, setQuery] = useState(searchParams.get('q') || '');

const { isSearching } = useDebounceSearch({
searchValue: query,
delay: 500,
onSearch: (value) => {
const params = new URLSearchParams(searchParams);
if (value) {
params.set('q', value);
} else {
params.delete('q');
}
router.push(`?${params.toString()}`);
},
});

return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search and update URL..."
/>
{isSearching && <p>Searching...</p>}
</div>
);
}

Configuration

These hooks are purely client-side and require no server configuration or providers. Both are marked with "use client" (via the search hook) and use standard React state and effects.

Delay Tuning

Use CaseRecommended DelayRationale
API search queries300-500msBalances responsiveness with server load reduction.
Local filtering150-250msFaster feedback since no network round-trip.
URL parameter sync500msAvoids excessive browser history entries.
Form validation300msProvides timely feedback without flicker.

Edge Cases and Gotchas

  • Empty String Handling: When searchValue becomes empty (or whitespace-only), useDebouncedSearch immediately calls onSearch('') without waiting for the debounce delay and sets isSearching to false. This ensures the UI clears promptly when the user erases their input.
  • Duplicate Prevention: The hook tracks the previous search value via a useRef. If the debounced value has not changed since the last search, onSearch is not called again. This prevents redundant API calls when the user types and then deletes back to the same text.
  • isSearching Composite State: isSearching is true in two cases: (1) the onSearch callback is currently executing, or (2) the raw searchValue differs from debouncedValue (meaning a debounce is pending). This provides a comprehensive "busy" indicator.
  • Cleanup on Unmount: useDebounceValue clears its timeout on unmount via the effect cleanup. No pending state updates will fire after the component is removed.
  • Async onSearch: The onSearch callback can be async. The hook awaits it and sets isSearching to false in a finally block, ensuring the loading state is always cleared even if the search throws an error.
  • clearSearch Purpose: Call clearSearch() when you need to reset the search state externally (e.g., when switching tabs or clearing filters programmatically). It resets the previousValue ref so the next search will always fire.
  • Generic Type Support: useDebounceValue is generic and works with any type (string, number, objects, arrays). However, for objects and arrays, ensure referential stability or the debounce will re-trigger on every render.
  • useUrlExtraction -- Debounce URL input before triggering extraction.
  • useInfiniteLoading -- Combine debounced search with infinite scroll for paginated search results.
  • useGeolocation -- Debounce location-based searches after coordinate changes.