Debounced search with controlled inputs

Maya Patel Jan 2026
1 tab
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useDebounce } from '@/hooks/useDebounce'
import api from '@/services/api'

export function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 300)

  const { data: results, isLoading } = useQuery({
    queryKey: ['search', debouncedSearchTerm],
    queryFn: async () => {
      if (!debouncedSearchTerm) return []
      const { data } = await api.get('/search', {
        params: { q: debouncedSearchTerm },
      })
      return data
    },
    enabled: debouncedSearchTerm.length > 2,
  })

  return (
    <div className="relative">
      <input
        type="search"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
        className="w-full px-4 py-2 pl-10 border rounded-lg"
      />
      <i className="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />

      {isLoading && (
        <div className="absolute right-3 top-1/2 transform -translate-y-1/2">
          <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
        </div>
      )}

      {results && results.length > 0 && (
        <div className="absolute top-full mt-2 w-full bg-white rounded-lg shadow-lg border max-h-96 overflow-y-auto">
          {results.map((result: any) => (
            <a
              key={result.id}
              href={`/posts/${result.id}`}
              className="block px-4 py-3 hover:bg-gray-50 border-b last:border-b-0"
            >
              <h4 className="font-medium">{result.title}</h4>
              <p className="text-sm text-gray-600">{result.excerpt}</p>
            </a>
          ))}
        </div>
      )}
    </div>
  )
}
1 file · typescript Explain with highlit

Search inputs that fire API requests on every keystroke create poor UX and waste server resources. Debouncing delays the search until typing pauses, reducing requests dramatically. I combine a controlled input component with a custom useDebounce hook that returns the debounced value. When the debounced value changes, React Query triggers a new search request. The input feels instantly responsive because it's controlled, while the search happens asynchronously after the debounce delay. I set the delay to 300-500ms based on input type—longer for expensive searches, shorter for simple filters. This pattern is essential for autocomplete, live search, and any input-driven queries.