Mastering Angular Signals with a Real-World Notification Service

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

AspectBehaviorSubject + ObservableSignal + asReadonly()Winner for Notifications
Code lengthMore boilerplateMuch shorterSignal
Subscription managementRequired (or async pipe)NoneSignal
Initial value guaranteeYes (BehaviorSubject)Yes (always synchronous)Tie
Fine-grained reactivityComponent level (OnPush + async)True fine-grained (only changed bindings)Signal
Encapsulation.asObservable().asReadonly()Tie
Async interopExcellent (operators, streams)Good (via toSignal, toObservable)RxJS if needed
Best forStreams, HTTP, WebSocketsUI state, queues, toggles, formsSignal 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

Leave a Reply