1. Why Signals for Notifications?
Notifications (toasts, alerts, snackbars) are one of the sweet-spot use cases for signals because:
- The state is synchronous most of the time → we always want the current list of visible messages.
- The state is shared across the app → via a root service.
- We need fine-grained reactivity → only the toast area should re-render when a message is added or removed.
- No complex async streams → just add → show → auto-remove after timeout.
- We want no subscriptions in components → no async pipe, no manual unsubscribe → fewer bugs for students.
Classic RxJS way (BehaviorSubject) → works, but:
- Requires .asObservable()
- Components need | async or .subscribe()
- Manual array immutability before .next()
- Easy to forget cleanup
Signals way → cleaner, safer, more performant for this scenario.
2. Implementation – Quick Review
notification.service.ts (your version)
import { Injectable, signal } from '@angular/core';
import { NotificationMessage } from './notification-message.model';
@Injectable({ providedIn: 'root' })
export class NotificationService {
private notifications = signal<NotificationMessage[]>([]);
notifications$ = this.notifications.asReadonly();
addNotification(notification: NotificationMessage) {
this.notifications.update((prev) => [...prev, notification]);
setTimeout(() => {
this.removeNotification(notification);
}, 50000); // note: 50 seconds
}
removeNotification(notification: NotificationMessage) {
this.notifications.update(current =>
current.filter(n => n !== notification)
);
}
}Model (notification-message.model.ts)
export interface NotificationMessage {
type: 'ERROR' | 'INFO';
message: string;
}Component template (notification.component.html)
<div class="notification-container">
<div *ngFor="let notification of notificationService.notifications$()"
class="notification"
[ngClass]="{'error': notification.type === 'ERROR', 'info': notification.type === 'INFO'}">
<button class="close-button" (click)="notificationService.removeNotification(notification)">
<img src="x-close.svg" alt="Close" />
</button>
<p>{{ notification.message }}</p>
</div>
</div>→ Problems to discuss: | async, possible null, subscription lifecycle, more code.
Enhancements
Add a computed signal for convenience
readonly hasNotifications = computed(() => this.notifications().length > 0);
readonly errorCount = computed(() => this.notifications().filter(n => n.type === 'ERROR').length);Usage in template:
@if (notificationService.hasNotifications()) {
<div class="badge">{{ notificationService.errorCount() }} errors</div>
}Optional: effect() for logging / analytics
constructor() {
effect(() => {
console.log(`Notifications changed → ${this.notifications().length} active`);
// Could send to analytics, play sound, etc.
});
}→ Warning: effects run on every change → use sparingly in services.
5. Comparison Table
| Aspect | BehaviorSubject + Observable | Signal + asReadonly() | Winner for Notifications |
|---|---|---|---|
| Code length | More boilerplate | Much shorter | Signal |
| Subscription management | Required (or async pipe) | None | Signal |
| Initial value guarantee | Yes (BehaviorSubject) | Yes (always synchronous) | Tie |
| Fine-grained reactivity | Component level (OnPush + async) | True fine-grained (only changed bindings) | Signal |
| Encapsulation | .asObservable() | .asReadonly() | Tie |
| Async interop | Excellent (operators, streams) | Good (via toSignal, toObservable) | RxJS if needed |
| Best for | Streams, HTTP, WebSockets | UI state, queues, toggles, forms | Signal here |
6. Summary & Takeaways
Your notification service is a textbook example of signals:
- Shared, synchronous UI state
- Simple add/remove logic
- Auto-timeout (async trigger → sync state update)
- Direct template binding without pipes
- Encapsulation via readonly view
Rule of thumb 2026:
- Use signals for: local state, derived values, toast/notification queues, form flags, UI toggles
- Use RxJS for: HTTP pipelines, WebSockets, debounce/throttle, complex combining
- Use both together: toSignal(http.get(…)) → feed into signal state