← Back to Docs
Recipe

Input Debounce with Cancellation

Prevent stale UI updates when a rapid-fire input stream outpaces async resolution.

Problem

A search box fires a fetch on every keystroke. The user types “meridian” in 400ms. Six requests race to the server. The response for “mer” arrives after “meridian”, overwriting correct results with stale data.

Strategy

  • Debounce — delay execution until the user pauses typing (250ms is a solid default).
  • Cancel — use an AbortController per keystroke. When a new keystroke arrives, abort the previous controller so its promise rejects immediately.
  • Guard — only apply the response if the query that produced it still matches the current input value.

Implementation

let controller: AbortController | null = null;
let timer: ReturnType<typeof setTimeout>;

function handleInput(value: string) {
  clearTimeout(timer);
  controller?.abort();

  timer = setTimeout(async () => {
    controller = new AbortController();
    try {
      const res = await fetch(`/api/search?q=${value}`, {
        signal: controller.signal,
      });
      const data = await res.json();
      if (currentQuery === value) setResults(data);
    } catch (err) {
      if ((err as Error).name !== "AbortError") throw err;
    }
  }, 250);
}

Edge Cases

Empty string

Skip the fetch entirely when the input is blank. Clear results immediately.

Rapid clear-and-type

AbortController handles this naturally — the clear-triggered fetch is aborted by the next keystroke.

Network error

Distinguish AbortError from genuine failures. Retry only on non-abort errors.

Component unmount

Abort the controller and clear the timer in a cleanup effect to prevent state updates on dead components.

All RecipesMeridian Docs