What is Debounce?

Debounce: Debouncing delays a function until its trigger has been quiet for a set time, so a search box sends one request after typing stops, not one per keystroke.

To debounce an event is to wait until it stops happening before reacting. A user typing "invoice" fires seven input events in under a second; a debounced search handler ignores the first six and runs once, 250 milliseconds after the last. Each new event resets the timer. The term comes from electronics, where a mechanical switch "bounces" and produces several signals for one press. In interfaces it is the standard answer to anything that fires rapidly and only needs the final state: search as you type, autosave, validating a username, resizing a window.

Debounce or throttle

  • Debounce waits for a pause. Use it when only the end result matters: search queries, autosave, validation that calls the server.
  • [Throttle](/glossary/throttle) runs at most once per interval while events keep coming. Use it when intermediate updates matter: scroll position, pointer tracking, progress.
  • Neither for things that must respond to every event, such as the characters appearing in the field itself. Debounce the expensive reaction, never the input.

How to build one in React

The simplest form is a hook that returns a delayed copy of a value. The input stays controlled and immediate, and the slow work is derived from the debounced value:

tsx
"use client"
import * as React from "react"
import { SearchInput } from "@/components/ui/search-input"

export function useDebouncedValue<T>(value: T, delay = 250) {
  const [debounced, setDebounced] = React.useState(value)
  React.useEffect(() => {
    const id = window.setTimeout(() => setDebounced(value), delay)
    return () => window.clearTimeout(id)
  }, [value, delay])
  return debounced
}

export function CustomerSearch({ search }: { search: (q: string, signal: AbortSignal) => Promise<string[]> }) {
  const [query, setQuery] = React.useState("")
  const q = useDebouncedValue(query.trim())
  const [results, setResults] = React.useState<string[]>([])
  React.useEffect(() => {
    if (!q) {
      setResults([])
      return
    }
    const ac = new AbortController()
    search(q, ac.signal).then(setResults).catch(() => {})
    return () => ac.abort()
  }, [q, search])
  return <SearchInput value={query} onChange={(e) => setQuery(e.target.value)} onClear={() => setQuery("")} placeholder="Search customers" />
}

The effect's cleanup clears the pending timer, so unmounting mid-typing never calls a stale function, and the AbortController cancels an in-flight request when a newer query starts, so results cannot arrive out of order. Typical delays are 150 to 300ms for search and 500ms to a second for autosave. Lodash's debounce adds leading and trailing options and a flush method for the cases that need a plain function instead of a hook.

Common mistakes

  • Creating the debounced function inside render without memoizing it, which makes a new timer on every render and never debounces anything.
  • Debouncing the input value so that typed characters appear late. Keep the field immediate and debounce the request.
  • No loading state during the delay, so a slow search looks broken. Show a small spinner once the request is actually in flight.
  • Forgetting the final call on blur or submit. If the user presses Enter before the timer fires, run the search at once.

MiniDev's SearchInput, Autocomplete and Combobox are controlled inputs that leave the timing to you, which is where the hook above fits. The SearchWithResults block wires a field and a result list together.

Search InputuiSearch input with a leading search icon and a clear button that appears once there is text. Comes in sm, default and lg sizes, controlled or uncontrolled.npx shadcn@latest add @minidev/search-inputComboboxuiShadcn style combobox: a popover select with a filter input, check mark on the chosen item and an empty message. Built on Base UI popover for React.npx shadcn@latest add @minidev/combobox