Angular Signals in der Praxis: RxJS gezielt ablösen
Die Signals-API von Angular ist inzwischen so ausgereift, dass sie viele reaktive Muster wirklich vereinfacht, für die bisher RxJS notwendig war. Es geht dabei nicht darum, RxJS vollständig zu ersetzen — Observables sind nach wie vor die richtige Wahl für asynchrone Event-Streams, HTTP-Kommunikation und komplexe Datenflüsse aus mehreren Quellen. Für lokalen Komponentenzustand und abgeleitete Werte sind Signals jedoch oft übersichtlicher und leichter zu verstehen.
Das Grundprinzip
Ein Signal ist ein reaktives Grundelement, das einen Wert hält und alle Konsumenten benachrichtigt, sobald sich dieser Wert ändert. Anders als ein Observable besitzt es immer einen aktuellen Wert, der synchron gelesen werden kann. Das beseitigt eine ganze Klasse von Lade- und Undefined-Boilerplate.
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
const doubled = computed(() => count() * 2);
effect(() => {
console.log(`count ist ${count()}, doubled ist ${doubled()}`);
});
count.set(5); // löst den Effect automatisch aus
Zum Vergleich die äquivalente Implementierung mit BehaviorSubject:
import { BehaviorSubject, combineLatest, map } from 'rxjs';
const count$ = new BehaviorSubject(0);
const doubled$ = count$.pipe(map(n => n * 2));
combineLatest([count$, doubled$]).subscribe(([c, d]) => {
console.log(`count ist ${c}, doubled ist ${d}`);
});
count$.next(5);
Beide Varianten funktionieren, aber die Signals-Version ist für diesen Anwendungsfall weniger aufwändig.
Zustandsverwaltung in Komponenten
Der größte Gewinn zeigt sich in Komponenten, die bisher mehrere BehaviorSubjects zur Verfolgung zusammengehöriger Zustände verwendet haben:
// Vorher: viele BehaviorSubjects
@Component({ ... })
export class FilterComponent {
private query$ = new BehaviorSubject('');
private category$ = new BehaviorSubject<string | null>(null);
readonly results$ = combineLatest([this.query$, this.category$]).pipe(
debounceTime(200),
switchMap(([query, category]) => this.api.search(query, category))
);
}
// Nachher: Signals für den Zustand, RxJS nur für den HTTP-Aufruf
@Component({ ... })
export class FilterComponent {
readonly query = signal('');
readonly category = signal<string | null>(null);
readonly results = toSignal(
toObservable(computed(() => ({ query: this.query(), category: this.category() }))).pipe(
debounceTime(200),
switchMap(({ query, category }) => this.api.search(query, category))
)
);
}
Die Bridge-Utilities toSignal und toObservable ermöglichen eine unkomplizierte Kombination beider Ansätze.
Wann RxJS die bessere Wahl bleibt
Signals sind kein vollständiger Ersatz. RxJS bleibt die richtige Wahl für:
- HTTP-Anfragen —
HttpClientliefert Observables; das Fehlerbehandlungsmodell ist etabliert - WebSocket- / SSE-Streams — kontinuierliche Event-Quellen passen natürlich zu Observables
- Komplexe Operatoren —
switchMap,mergeMap,debounceTime,retryhaben kein Signal-Äquivalent - Router-Events —
Router.eventsist ein Observable-Stream
Eine gute Faustregel: Signals für Zustand innerhalb einer Komponente oder eines Service, RxJS an den Grenzen zwischen der Anwendung und der Außenwelt.
OnPush und Signals
Ein wichtiger Vorteil, der besonders hervorzuheben ist: Komponenten mit Signals funktionieren mit ChangeDetectionStrategy.OnPush ohne jeden zusätzlichen Aufwand. Angulars Change Detection versteht Signals nativ und rendert nur dann neu, wenn sich ein im Template gelesenes Signal tatsächlich ändert.
Das war bisher ein häufiger Schmerzpunkt mit OnPush — man musste sorgfältig steuern, welche Observables über die async-Pipe abonniert werden, und vergessene markForCheck-Aufrufe führten zu schwer nachvollziehbaren Bugs. Mit Signals übernimmt Angular dieses Tracking automatisch.
Pragmatischer Migrationspfad
Wer eine bestehende Angular-Anwendung betreut, muss nichts auf einmal umschreiben. Der pragmatische Ansatz:
- Neue Komponenten: von Beginn an mit Signals schreiben
- Bestehende Komponenten: migrieren, wenn die Datei ohnehin angefasst wird
- Services: RxJS beibehalten für alles, was HTTP oder komplexe Stream-Koordination erfordert
- Geteilter Zustand über Komponenten hinweg: Signal-basierte Store-Patterns evaluieren
Das Angular-Team hat die Migration bewusst schrittweise gestaltet — es gibt keine erzwungene Entscheidung zwischen beiden Systemen.
Gedanken dazu? Einfach direkt schreiben.
Artikel diskutieren