Reputation: 103
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(); }
});
Upvotes: 10
Views: 6612
Reputation: 6874
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(),
});
}
Upvotes: 9
Reputation: 3319
Similar to @an-nguyen's answer here, you can achieve a debounce
effect without RxJS
@Component({
selector: "app-search",
standalone: true,
imports: [FormsModule],
template: `
<input
[ngModel]="searchTerm()"
(ngModelChange)="searchTerm.set($event)" />
`
})
export class SearchComponent {
public searchTerm = model<string>("");
private debounceTime = 300;
private timeout: ReturnType<typeof setTimeout> | null;
private searchEffect = effect(() => this.searchChange(this.searchTerm()));
searchChange(search: string): void {
if (!search) return;
clearTimeout(this.timeout); // Prevent previous input changes from searching.
this.timeout = setTimeout(() => {
alert(search); // handle search
}, this.debounceTime);
}
}
Upvotes: 0
Reputation: 1
None of answers worked for me. With some experiments and according to this: https://www.techiediaries.com/angular-16-convert-signals-to-observables/ My solution to this problem:
<input type="text" class="form-control" [ngModel] ="searchTerm()
(input)="searchTerm.set(input.value)" #input>
@Component
searchTerm = signal('');
debouncedSearchTerm = signal('')
obs$ = toObservable(this.searchTerm);
ngOnInit(): void {
this.obs$
.pipe(debounceTime(500))
.subscribe((val) => this.debouncedSearchTerm.set(val));
}
Upvotes: 0
Reputation: 39
I recommend to do it this way:
function debounceTimeSignal<T>(valueSignal: Signal<T>, time: number = 0): Signal<T> {
return toSignal(toObservable(valueSignal).pipe(debounceTime(time)), { initialValue: valueSignal() });
}
Upvotes: 3
Reputation: 91
Something like
searchFor = toSignal(toObservable(this.searchTerm).pipe(debounceTime(100)), {
initialValue: '',
});
Upvotes: 9
Reputation: 149
There are no built-in solution for debounce in Signal. However, you can create a custom function to do that:
function debouncedSignal<T>(input: Signal<T>, timeOutMs = 0): Signal<T> {
const debounceSignal = signal(input());
effect(() => {
const value = input();
const timeout = setTimeout(() => {
debounceSignal.set(value);
}, timeOutMs);
return () => {
clearTimeout(timeout);
};
});
return debounceSignal;
}
const itemsList = [
{ name: 'Product A', category: 'Category 1' },
{ name: 'Product B', category: 'Category 2' },
{ name: 'Product C', category: 'Category 1' },
];
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<input [ngModel]="searchTerm()" (ngModelChange)="searchTerm.set($any($event))">
<ul>
<li *ngFor="let item of items">
{{item.name}}
</li>
</ul>
`,
})
export class App {
items = itemsList;
searchTerm = signal('');
debounceSearchValue = debouncedSignal(this.searchTerm, 500);
constructor() {
effect(() => {
this.search(this.debounceSearchValue());
});
}
private search(value: string): void {
if (!value) {
this.items = itemsList;
}
const query = value.toLowerCase();
this.items = itemsList.filter(
(item) =>
item.name.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query)
);
}
}
This solution is the way so complicate, so I recommend to use RxJS for cleaner and more efficient code
Upvotes: 4