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.
// 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
}} />;
}A runnable project for this example, opened in the bext play editor — edit the code and the preview updates.
Live-editable — the preview recompiles as you type.