Search-as-you-type (debounced island)

A signals island wires a search input to /api/search with two essentials: a 200ms debounce so we don't fire one request per keystroke, and an AbortController per request so a slow earlier response can't overwrite a faster later one. State (query, results, pending, RTT) lives in a single signal; the island re-renders on every write.

Tip
The AbortController cancels the network request, not just the result handler — it frees server resources for partially-transmitted request bodies. Without it, slow requests can pile up in bursts and exhaust the connection pool.

Try it


type to search

Try typing fast — only the final query hits the network. Corpus is 20 phrases; the API adds 80ms artificial latency.

src/components/SearchDebounce.tsxTSX
// SearchDebounce.tsx — signals island.
"use signals";
import { signal } from "@bext-stack/framework/signals";

export default function SearchDebounce() {
  const state = signal({ q: "", results: [], pending: false });

  let timer = null;
  let inflight = null;
  const fire = (q) => {
    if (inflight) inflight.abort();      // cancel slower in-flight
    if (!q) { state.value = { ...state.value, results: [] }; return; }
    state.value = { ...state.value, pending: true };
    const ac = new AbortController();
    inflight = ac;
    fetch("/api/search?q=" + encodeURIComponent(q), { signal: ac.signal })
      .then(r => r.json())
      .then(d => state.value = { ...state.value, results: d.hits, pending: false });
  };

  return <input onInput={e => {
    const q = e.target.value;
    state.value = { ...state.value, q };
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => fire(q), 200);  // 200ms debounce
  }} />;
}
Try it liveOpen in play

A runnable project for this example, opened in the bext play editor — edit the code and the preview updates.

src/app/page.tsx2 files · runs in your browser
Live-editable — the preview recompiles as you type.