I am trying to create a toast service for my e-commerce project. Initially, this was my code below. There is a toasts array and an add function. We can pass in a message to the add function and it will put it in the toasts array. Then the frontend will render it, and a setTimeout will run. After 5 seconds, the message will be removed from the toasts array and change detection will trigger because of setTimeout and the frontend will stop rendering the message.
// toast.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ToastService {
toasts: { message: string; duration: number; type: 'success' | 'error' }[] = [];
add(message: string, duration: number = 3000, type: 'success' | 'error' = 'success') {
this.toasts.push({ message, duration, type });
setTimeout(() => this.remove(0), duration);
}
remove(index: number) {
this.toasts.splice(index, 1);
}
}
but the problem was, after setTimeout completes, the toast message was removed from the array, but the message was still showing in my page ...
later I used signals, and that problem was resolved.
@Injectable({
providedIn: 'root',
})
export class ToastService {
toasts = signal<ToastInfo[]>([]);
create(message: string, type: AlertType): void {
const toastItem: ToastInfo = { message, type };
this.toasts.update(t => [...t, toastItem]);
of(toastItem) //creating a observale
.pipe(delay(5000)) // inserting into pipe
.subscribe(() => {
this.remove(toastItem) //emitting
})
}
remove(value: ToastInfo) {
this.toasts.update(t => t.filter(item => item !== value));
console.log(this.toasts);
}
}
My question is: Why was it not working with setTimeout? Angular will run its change detection after setTimeout completes, and then the array will be empty, and there will be no message to render...
It might be due to Zoneless Change detection, check if you have provideZonelessChangeDetection set in main.ts.
Angular 21 marked zoneless change detection as stable and will be the future of Change Detection in Angular.
The property updates will not trigger change detection (like how it worked pre zoneless), from the documentation as below.
Angular relies on notifications from core APIs in order to determine when to run change detection and on which views. These notifications include:
- ChangeDetectorRef.markForCheck (called automatically by AsyncPipe)
- ComponentRef.setInput
- Updating a signal that's read in a template
- Bound host or template listeners callbacks
- Attaching a view that was marked dirty by one of the above
So if you have a property that requires change detection to be fired, we must be a signal, you can also use markForCheck or detectChanges of ChangeDetectorRef.
It is important to note that a signal marks a change when the actual value has changed for primitives (string, number, etc.) and memory reference change for non primitives (array, object, etc).
To solve it, just convert the toasts array to a signal array, so that when we update change detection picks this up and updates the UI.
// toast.service.ts
import { Injectable } from '@angular/core';
export interface Toast {
message: string;
duration: number;
type: 'success' | 'error'
};
@Injectable({
providedIn: 'root',
})
export class ToastService {
toasts = signal<Toast[]>([]);
add(message: string, duration: number = 3000, type: 'success' | 'error' = 'success') {
this.toasts.update((toastPrev: Toast[]) =>
([...toastPrev, { message, duration, type }]));
setTimeout(() => this.remove(0), duration);
}
remove(index: number) {
this.toasts.update((toastPrev: Toast[]) => {
// arrays and objects are stored as memory references
toastPrev.splice(index, 1);
// toastPrev will contain the same reference after splicing.
return [...toastPrev]; // new array instance must be returned for non primitive signals, so that memory reference is different which marks the signal as changed.
});
}
}
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