Search Form with Debounce
Problem
A search input should trigger an API query as the user types, but not on every keystroke. Requests must be debounced so only the final value after a pause is sent, and stale responses must be ignored.
Solution
Use connect() with validateOnChange and debounce to schedule async work after the user stops typing:
ts
import { createForm } from '@vielzeug/forge';
const form = createForm({
defaultValues: { query: '', category: 'all', sortBy: 'relevance' },
});
const queryConn = form.connect('query', {
validateOnChange: true,
debounce: 300,
});
// Register an async "validator" that runs the search and updates results
form.setValidator('query', async (value, signal) => {
if (!value || String(value).length < 2) {
updateResultsUI([]);
return undefined;
}
const category = form.get('category');
const sortBy = form.get('sortBy');
const url = `/api/search?q=${encodeURIComponent(String(value))}&category=${category}&sort=${sortBy}`;
const response = await fetch(url, { signal });
const results = await response.json();
updateResultsUI(results);
return undefined; // no error to display
});
function updateResultsUI(results: unknown[]) {
console.log('Search results:', results);
}Pitfalls
- The validator receives an
AbortSignal. Always pass it tofetchso stale requests are cancelled automatically when a newer keystroke triggers a new run. - Subscribing to the full form state and triggering search inside
subscribe()fires on every field change, including programmatic writes. UsingsubscribeField('query', ...)scopes the trigger correctly. - Setting
debouncetoo low defeats the purpose; 200–500 ms is a reasonable default for remote searches.