I have a signal that is bound to an input field. I'm trying to define an effect() for the searchTerm, but because it's user input, I'd like to debounce that effect (i.e. rxjs) so that the search doesn't happen with each keystroke. I'm unclear on how to accomplish this and the documentation doesn't really cover the situation.
<input [ngModel]="searchTerm()" (ngModelChange)="searchTerm.set($event)">
effect(() => {
if (this.searchTerm() !== '') { this.search(); }
});
Something like
searchFor = toSignal(toObservable(this.searchTerm).pipe(debounceTime(100)), {
initialValue: '',
});
Update: added Observable based version based on @Alexey's approach.
Debounced signals can be achieved using the following utility function. This is a modified version of the idea of @An Nguyen, which contained two bugs.
One of the real power of signals lies in the fact that it is easy to create standalone functions that create a signal (or effect). This allows for easy sharing, and for nicely applying "composition over inheritance".
export function debouncedSignal<T>(
sourceSignal: Signal<T>,
debounceTimeInMs = 0
): Signal<T> {
const debounceSignal = signal(sourceSignal());
effect(
(onCleanup) => {
const value = sourceSignal();
const timeout = setTimeout(
() => debounceSignal.set(value),
debounceTimeInMs
);
// The `onCleanup` argument is a function which is called when the effect
// runs again (and when it is destroyed).
// By clearing the timeout here we achieve proper debouncing.
// See https://angular.io/guide/signals#effect-cleanup-functions
onCleanup(() => clearTimeout(timeout));
},
{ allowSignalWrites: true }
);
return debounceSignal;
}
Usage:
debouncedSearchTerm = debouncedSignal(this.searchTerm, 250);
One can argue about how far one should go with avoiding observables. Advanced observable usage can be hard to read and understand, but the same applies to advanced signal + effect usage, such as the case above. In this specific case implementing the same function with observables is clearly a lot simpler:
export function debouncedSignal<T>(
sourceSignal: Signal<T>,
debounceTimeInMs = 0,
): Signal<T> {
const source$ = toObservable(sourceSignal);
const debounced$ = source$.pipe(debounceTime(debounceTimeInMs));
return toSignal(debounced$, {
initialValue: sourceSignal(),
});
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With