To complement @brunnerh's answer, you can compose this into a complete utility like so:
export function debounce<T>(f: (...args: T[]) => unknown, ms: number) {
let id: null | number = null;
return (...args: T[]) => {
if (id) {
clearTimeout(id);
}
id = setTimeout(() => {
f(...args);
}, ms);
};
}
export function debounced<T>(stateGetter: () => T, ms: number) {
let state = $state(stateGetter());
const update = debounce<T>((v) => (state = v), ms);
$effect(() => update(stateGetter()));
return () => state;
}
Which you can then use like so:
let getDebouncedSearch = debounced(() => search, 500)
However, debounced values are usually more useful for API calls and asynchronous requests. You generally don't need the debounced value itself in the UI, so you very probably do not need a signal for it.
What you probably need is simply:
const search = $state("");
$effect(() => {
debounced(() => getSearchResults(search), 500);
});