Recherche au fil de la saisie (îlot avec anti-rebond)

Un îlot à signaux relie un champ de recherche à /api/search avec deux mécanismes essentiels : un anti-rebond de 200 ms pour ne pas déclencher une requête à chaque frappe, et un AbortController par requête pour qu'une réponse lente antérieure ne puisse pas écraser une réponse plus rapide ultérieure. L'état (requête, résultats, en attente, RTT) vit dans un seul signal ; l'îlot se re-rend à chaque écriture.

Astuce
L'AbortController annule la requête réseau, pas seulement le traitement du résultat — cela libère les ressources du serveur pour les corps de requête partiellement transmis. Sans lui, des requêtes lentes peuvent s'accumuler en rafale et saturer le pool de connexions.

Essayez


type to search

Tapez vite — seule la dernière requête touche le réseau. Le corpus contient 20 phrases ; l'API ajoute 80 ms de latence artificielle.

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
  }} />;
}