Debounced search
Search-as-you-type without the usual race conditions. createDebounced smooths the input, async re-runs whenever the debounced value changes, and onCleanup aborts the previous fetch before the next one starts.
The recipe
import { signal, onCleanup } from "elements-kit/signals";import { async } from "elements-kit/utilities/async";import { createDebounced } from "elements-kit/utilities/debounced";
const query = signal("");const debounced = createDebounced(query, 250);
const search = async(() => { const q = debounced(); // tracked if (!q) return []; // skip empty query const ctrl = new AbortController(); onCleanup(() => ctrl.abort()); // abort on re-run return fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: ctrl.signal, }).then((r) => r.json());}).start();Type apr and the body re-runs three times — but only the last fetch survives. The first two are aborted by onCleanup before they resolve.
How each piece contributes
| Primitive | Role |
|---|---|
signal (query) | Bound to the input’s on:input event. |
createDebounced(query, 250) | Computed that mirrors query but only emits after 250 ms of silence. |
async(() => …).start() | Re-runs the body whenever debounced() changes. Read it before any await to be tracked. |
onCleanup(() => ctrl.abort()) | Runs when the body re-fires — cancels in-flight requests. |
AbortController + fetch({ signal }) | The cancellation primitive itself. fetch rejects with AbortError when aborted. |
Variations
Retry on transient failure — wrap the fetch in retry, same as Data fetching:
import { retry } from "elements-kit/utilities/retry";
const search = async(() => { const q = debounced(); if (!q) return []; return retry(() => { const ctrl = new AbortController(); onCleanup(() => ctrl.abort()); return fetch(`/api/search?q=${q}`, { signal: ctrl.signal }) .then((r) => r.json()); }, 2, (n) => n * 250)();}).start();Throttle instead of debounce — when intermediate results should appear while the user is still typing:
import { createThrottled } from "elements-kit/utilities/throttled";const throttled = createThrottled(query, 250);Refresh on tab return — read windowFocused() inside the body to refetch when the user comes back:
import { windowFocused } from "elements-kit/utilities/window-focus";
const search = async(() => { windowFocused(); // re-runs on focus const q = debounced(); // …}).start();Gotchas
See also
- Data fetching — same
async+onCleanup+ abort pattern, plus retry, online, focus. - Utilities —
createDebounced,createThrottled,retry. - Async — full
async/Asyncreference.