Rreact.wiki
React Snippets

Debounced search input with useEffect and useRef

FormsdebouncesearchuseEffectuseRefforms

A reusable search input that delays API calls until user stops typing for a specified duration.

TSX
import { useState, useEffect, useRef } from 'react';
 
export function DebouncedSearchInput({
  onSearch,
  delay = 300,
}: {
  onSearch: (query: string) => void;
  delay?: number;
}) {
  const [query, setQuery] = useState('');
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
 
  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
 
    if (query.trim() !== '') {
      timeoutRef.current = setTimeout(() => {
        onSearch(query);
      }, delay);
    }
 
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [query, delay, onSearch]);
 
  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
      aria-label="Search input"
    />
  );
}
TSX
import { useState, useEffect, useRef } from 'react';
 
export function DebouncedSearchInput({
  onSearch,
  delay = 300,
}: {
  onSearch: (query: string) => void;
  delay?: number;
}) {
  const [query, setQuery] = useState('');
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
 
  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
 
    if (query.trim() !== '') {
      timeoutRef.current = setTimeout(() => {
        onSearch(query);
      }, delay);
    }
 
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [query, delay, onSearch]);
 
  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
      aria-label="Search input"
    />
  );
}

This component accepts a onSearch callback and optional delay (in ms) to trigger search only after typing pauses. It uses useRef to persist the timeout ID across renders and useEffect to reset/clear it on every query change. Important: always clean up the timeout in the effect’s cleanup function to avoid memory leaks or stale callbacks. Usage: <DebouncedSearchInput onSearch={(q) => fetchResults(q)} delay={500} />. Note that onSearch is called with the current query, including empty strings — add if (query.trim()) inside onSearch if you want to skip empty queries.