Dieser Artikel erklärt den Unterschied zwischen Angular Signals und RxJS, wann man das eine und wann das andere verwendet und wie man sie in echten Angular-Anwendungen kombiniert.
Winkelsignale und RxJS sind keine zwei Werkzeuge, die miteinander im Krieg sind. Dabei handelt es sich um zwei unterschiedliche Tools, die zur Lösung unterschiedlicher Probleme entwickelt wurden und in modernen Angular-Anwendungen oft sehr gut zusammenarbeiten.
Das Problem entsteht, wenn wir sie vergleichen, als wären sie hundertprozentig austauschbar. Tatsächlich eignen sich Signale hervorragend zur Darstellung eines aktuellen, lesbaren und synchron ableitbaren Zustands. RxJS hingegen eignet sich perfekt für die Modellierung zeitasynchroner Streams, Ereignisse, Löschungen, Entprellungen, Wiederholungsversuche, WebSockets und komplexe Kombinationen von Streams.
Die richtige Frage lautet daher nicht: „Signale oder RxJS?“. Die richtige Frage lautet: „Verwalte ich einen aktuellen Zustand oder verwalte ich einen Fluss von Ereignissen im Laufe der Zeit?“.
Das mentale Modell: Barwert vs. Fluss über die Zeit
A Signal ist wie eine reaktive Variable. Enthält einen aktuellen Wert. Wenn sich dieser Wert ändert, weiß Angular, welche Teile der Schnittstelle aktualisiert werden müssen.
Ein Observable RxJS ist eine Folge von Werten über die Zeit. Es kann null, einen oder viele Werte ausgeben. Es kann abgeschlossen werden, es kann einen Fehler auslösen, es kann abbestellt werden und es kann mit Operatoren wie map, filter, switchMap, debounceTime, Wiederholen und viele mehr.
| Szenario | Signale | RxJS |
|---|---|---|
| Lokaler UI-Status | Ausgezeichnet | Möglich, aber oft übertrieben |
| Abgeleitete Werte | Ausgezeichnet mit berechnet |
Möglich mit Operatoren |
| HTTP-Einzelanruf | Nützlich zum Speichern des Ergebnisses | Großartig, weil HttpClient Observable |
| Suche entprellen | Zu kombinieren mit RxJS | Ausgezeichnet |
| WebSocket | Nützlich zum Anzeigen des letzten Status | Ideal zum Streamen |
| Löschung von Anfragen | Nicht sein bester Fall | Ausgezeichnet mit switchMap |
| Vorlage Angular | Sehr gut lesbar | Sehr gut mit async Pipe |
Was sind Winkelsignale
Signale sind ein in Angular integriertes Reaktionssystem. Ein Signal enthält einen Wert und ermöglicht es Angular, automatisch zu verfolgen, wer diesen Wert liest.
Wenn eine Komponente ein Signal in der Vorlage liest, kann Angular diesen Teil der Benutzeroberfläche aktualisieren, wenn sich das Signal ändert. Dies macht den Zustand expliziter und verringert den Bedarf an vielen auf Subjekten oder Verhaltenssubjekten basierenden Mustern für einfache Fälle.
import { computed, signal } from '@angular/core';
const count = signal(0);
const double = computed(() => count() * 2);
function increment(): void {
count.update((value) => value + 1);
}
In diesem Beispiel ist count der Hauptzustand, während double ein abgeleiteter Zustand ist. Wir müssen double nicht manuell aktualisieren: Angular berechnet es neu, wenn sich count.
Wenn die Signale ideal sind
- Wenn Sie den lokalen Status einer Komponente verwalten müssen.
- Wenn Sie Teile der Benutzeroberfläche ein- oder ausblenden müssen.
- Wenn Sie lokale Filter verwalten müssen.
- Wenn Sie abgeleitete Werte berechnen müssen.
- Wenn Sie unnötige manuelle Abonnements vermeiden möchten.
- Wenn Sie einen einfachen Store für eine Funktion erstellen möchten.
- Wenn der Zustand synchron ist und immer „den aktuellen Wert“ darstellt.
Was ist RxJS in Angular
RxJS ist eine reaktive Programmierbibliothek basierend auf Observable. In Angular wird es seit vielen Jahren verwendet und ist immer noch von zentraler Bedeutung für viele APIs des Frameworks.
HttpClient gibt Observable zurück. Die reaktiven Formen machen valueChanges und statusChanges verfügbar. Der Router stellt Parameter, Abfrageparameter und Ereignisse als Observables bereit. Auch WebSockets, Timer, DOM-Ereignisse und komplexe Streams lassen sich sehr gut mit RxJS modellieren.
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((query) => this.api.searchProducts(query))
);
Dieses Beispiel ist typisch: Der Benutzer schreibt in ein Suchfeld, wir warten 300 Millisekunden, wir ignorieren gleiche Werte und löschen automatisch die vorherige Anfrage, wenn eine neue Suche eintrifft.
Wenn RxJS ideal ist
- Wenn Sie zeitlich asynchrone Ereignisse haben.
- Wenn Sie entprellen oder drosseln müssen.
- Wenn Sie frühere Anfragen löschen müssen.
- Wenn Sie mehrere asynchrone Quellen kombinieren müssen.
- Bei der Arbeit mit WebSockets oder Live-Streams.
- Wenn Sie Wiederholungsversuche, Zeitüberschreitungen oder Abfragen verwalten müssen.
- Wenn Sie komplexe Abläufe orchestrieren müssen.
Einfache Faustregel
Verwenden Sie Signale, um den aktuellen Status der Benutzeroberfläche darzustellen.
Verwenden Sie berechnet für vom Zustand abgeleitete Werte.
Verwenden Sie RxJS für asynchrone Ereignisse, Zeitströme und Löschlogik.
Verwenden Sie toSignal, wenn Sie ein Observable als lesbaren Zustand in der Vorlage verfügbar machen möchten.
Verwenden Sie toObservable, wenn ein Signal in eine RxJS-Pipeline eingegeben werden muss.
Realbeispiel 1: Produktliste mit lokalen Filtern
Stellen wir uns eine E-Commerce-Seite mit einer Liste bereits geladener Produkte vor. Der Benutzer kann nach Text, Kategorie und Verfügbarkeit filtern. In diesem Fall sind Signale eine ganz natürliche Wahl.
Der Hauptstatus besteht aus Produkten, Abfrage, ausgewählter Kategorie und der Markierung „Nur verfügbar“. Die gefilterten Produkte sind ein abgeleiteter Zustand.
import { computed, inject, Injectable, signal } from '@angular/core';
import { firstValueFrom } from 'rxjs';
export interface Product {
id: number;
name: string;
category: string;
price: number;
stock: number;
}
@Injectable()
export class ProductsStore {
private readonly api = inject(ProductsApiService);
private readonly _products = signal<Product[]>([]);
private readonly _loading = signal(false);
private readonly _error = signal<string | null>(null);
private readonly _query = signal('');
private readonly _category = signal<string | null>(null);
private readonly _onlyAvailable = signal(false);
readonly products = this._products.asReadonly();
readonly loading = this._loading.asReadonly();
readonly error = this._error.asReadonly();
readonly query = this._query.asReadonly();
readonly category = this._category.asReadonly();
readonly onlyAvailable = this._onlyAvailable.asReadonly();
readonly filteredProducts = computed(() => {
const query = this._query().toLowerCase().trim();
const category = this._category();
const onlyAvailable = this._onlyAvailable();
return this._products().filter((product) => {
const matchesQuery = product.name.toLowerCase().includes(query);
const matchesCategory = !category || product.category === category;
const matchesAvailability = !onlyAvailable || product.stock > 0;
return matchesQuery && matchesCategory && matchesAvailability;
});
});
readonly total = computed(() => this.filteredProducts().length);
async loadProducts(): Promise<void> {
this._loading.set(true);
this._error.set(null);
try {
const products = await firstValueFrom(this.api.getProducts());
this._products.set(products);
} catch {
this._error.set('Impossibile caricare i prodotti.');
} finally {
this._loading.set(false);
}
}
setQuery(query: string): void {
this._query.set(query);
}
setCategory(category: string | null): void {
this._category.set(category);
}
setOnlyAvailable(value: boolean): void {
this._onlyAvailable.set(value);
}
}
Dieser Shop ist leicht zu lesen. Die Hauptdaten sind Signal. Die abgeleiteten Daten werden berechnet. Die Komponente kann einfach den Status anzeigen.
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
@Component({
selector: 'app-products-page',
standalone: true,
template: `
<section class="products-page">
<h2>Prodotti</h2>
<input
type="search"
placeholder="Cerca prodotto..."
[value]="store.query()"
(input)="store.setQuery($any($event.target).value)"
/>
<label>
<input
type="checkbox"
[checked]="store.onlyAvailable()"
(change)="store.setOnlyAvailable($any($event.target).checked)"
/>
Solo disponibili
</label>
@if (store.loading()) {
<p>Caricamento...</p>
} @else if (store.error()) {
<p class="error">{{ store.error() }}</p>
} @else {
<p>Risultati: {{ store.total() }}</p>
@for (product of store.filteredProducts(); track product.id) {
<article class="product-card">
<h3>{{ product.name }}</h3>
<p>Categoria: {{ product.category }}</p>
<p>Prezzo: {{ product.price }} €</p>
</article>
}
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductsPageComponent implements OnInit {
readonly store = inject(ProductsStore);
ngOnInit(): void {
void this.store.loadProducts();
}
}
Hier wäre die Verwendung von RxJS für jeden kleinen Filter möglich, aber wahrscheinlich ausführlicher. Signale machen den Code direkter.
Echtes Beispiel 2: Backend-Suche mit Debounce und Clear
Stellen wir uns nun eine Suche vor, die das Backend immer dann abfragt, wenn sich der Text ändert. In diesem Fall ist RxJS viel geeigneter.
Der Grund ist einfach: Wir möchten nicht für jeden Charakter eine Anfrage senden. Wir möchten einen kleinen Zeitraum warten, gleiche Werte ignorieren, die vorherige Anfrage abbrechen und alle Fehler behandeln.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
catchError,
debounceTime,
distinctUntilChanged,
map,
of,
shareReplay,
startWith,
switchMap
} from 'rxjs';
@Component({
selector: 'app-product-search',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<section>
<h2>Cerca prodotti</h2>
<input
type="search"
[formControl]="searchControl"
placeholder="Scrivi almeno 2 caratteri..."
/>
@if (results$ | async; as results) {
@for (product of results; track product.id) {
<p>{{ product.name }}</p>
}
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductSearchComponent {
private readonly api = inject(ProductsApiService);
readonly searchControl = new FormControl('', { nonNullable: true });
readonly results$ = this.searchControl.valueChanges.pipe(
startWith(this.searchControl.value),
map((value) => value.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap((query) => {
if (query.length < 2) {
return of([]);
}
return this.api.searchProducts(query).pipe(
catchError(() => of([]))
);
}),
shareReplay({ bufferSize: 1, refCount: true })
);
}
Hier ist switchMap entscheidend. Wenn der Benutzer „iph“ und dann sofort „iphone“ eingibt, kann die vorherige Anfrage abgebrochen werden und die Komponente verwendet nur das aktuellste Ergebnis.
Dies ist ein klassisches Beispiel, bei dem RxJS natürlicher ist als Signale.
Reales Beispiel 3: Gemeinsame Verwendung von RxJS und Signalen
In vielen Fällen besteht die beste Lösung darin, RxJS für den asynchronen Fluss und Signale zu verwenden, um das Ergebnis der Benutzeroberfläche anzuzeigen.
Wir können von einem Observablen ausgehen und es mit toSignal.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
catchError,
debounceTime,
distinctUntilChanged,
map,
of,
startWith,
switchMap
} from 'rxjs';
@Component({
selector: 'app-product-search-signal',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<section>
<input
type="search"
[formControl]="searchControl"
placeholder="Cerca prodotto..."
/>
@for (product of results(); track product.id) {
<p>{{ product.name }}</p>
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductSearchSignalComponent {
private readonly api = inject(ProductsApiService);
readonly searchControl = new FormControl('', { nonNullable: true });
private readonly results$ = this.searchControl.valueChanges.pipe(
startWith(this.searchControl.value),
map((value) => value.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap((query) => {
if (query.length < 2) {
return of([]);
}
return this.api.searchProducts(query).pipe(
catchError(() => of([]))
);
})
);
readonly results = toSignal(this.results$, {
initialValue: [] as Product[]
});
}
Die Pipeline bleibt RxJS, da sie Zeit, Entprellung und Löschung bewältigen muss. Da die Vorlage jedoch ein Signal liest, ist es einfacher.
Realbeispiel 4: Produktdetail mit Routenparameter
Ein weiteres sehr häufiges Szenario ist eine Detailseite. Das zu ladende Produkt hängt von der in der Route vorhandenen ID ab.
Der Routenparameter ist ein Stream. Der HTTP-Aufruf ist ein Stream. Die Benutzeroberfläche hingegen möchte nur den aktuellen Status des Produkts.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { catchError, distinctUntilChanged, map, of, switchMap } from 'rxjs';
@Component({
selector: 'app-product-detail-page',
standalone: true,
template: `
@if (product(); as product) {
<article>
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
<p>Prezzo: {{ product.price }} €</p>
</article>
} @else {
<p>Prodotto non trovato.</p>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductDetailPageComponent {
private readonly route = inject(ActivatedRoute);
private readonly api = inject(ProductsApiService);
private readonly product$ = this.route.paramMap.pipe(
map((params) => params.get('id')),
distinctUntilChanged(),
switchMap((id) => {
if (!id) {
return of(null);
}
return this.api.getProductById(id).pipe(
catchError(() => of(null))
);
})
);
readonly product = toSignal(this.product$, {
initialValue: null as Product | null
});
}
Hier ist die beste Wahl nicht nur Signale oder nur RxJS. Die beste Wahl ist, beides zu verwenden: RxJS, um auf die Kursänderung zu reagieren, und Signale, um den aktuellen Wert in der Vorlage anzuzeigen.
Echtes Beispiel 5: Warenkorb und Bestellsumme
Ein Wagen ist ein perfektes Beispiel für Signale. Der Warenkorb enthält eine Liste von Zeilen. Aus diesen Zeilen können wir die Gesamtmenge, die Zwischensumme, die Steuern und die Endsumme ableiten.
import { computed, Injectable, signal } from '@angular/core';
export interface CartItem {
productId: number;
name: string;
quantity: number;
unitPrice: number;
}
@Injectable({ providedIn: 'root' })
export class CartStore {
private readonly _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly totalQuantity = computed(() =>
this._items().reduce((total, item) => total + item.quantity, 0)
);
readonly subtotal = computed(() =>
this._items().reduce(
(total, item) => total + item.quantity * item.unitPrice,
0
)
);
readonly vat = computed(() => this.subtotal() * 0.22);
readonly total = computed(() => this.subtotal() + this.vat());
addItem(item: CartItem): void {
this._items.update((items) => {
const existing = items.find((current) => current.productId === item.productId);
if (!existing) {
return [...items, item];
}
return items.map((current) =>
current.productId === item.productId
? { ...current, quantity: current.quantity + item.quantity }
: current
);
});
}
removeItem(productId: number): void {
this._items.update((items) =>
items.filter((item) => item.productId !== productId)
);
}
clear(): void {
this._items.set([]);
}
}
In diesem Fall wäre die Verwendung von RxJS möglich, aber Signale sind einfacher. Die Summe ergibt sich immer aus dem aktuellen Stand. Es besteht keine Notwendigkeit, über einen Zeitplan der Ereignisse nachzudenken.
Echtes Beispiel 6: Live-Benachrichtigungen mit WebSocket
Live-Benachrichtigungen hingegen sind ein viel geeigneterer Fall für RxJS. Ein WebSocket sendet im Laufe der Zeit Nachrichten. Kann abbrechen, die Verbindung wiederherstellen, Fehler ausgeben und kontinuierliche Ereignisse erzeugen.
import { Injectable } from '@angular/core';
import { retry, share } from 'rxjs';
import { webSocket } from 'rxjs/webSocket';
export interface NotificationMessage {
id: string;
title: string;
body: string;
createdAt: string;
}
@Injectable({ providedIn: 'root' })
export class NotificationsService {
readonly messages$ = webSocket<NotificationMessage>(
'wss://example.com/notifications'
).pipe(
retry({ delay: 1000 }),
share()
);
}
Wenn wir die neueste Benachrichtigung in der Vorlage anzeigen möchten, können wir sie in ein Signal umwandeln.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-notification-bell',
standalone: true,
template: `
@if (lastMessage(); as message) {
<button type="button">
🔔 {{ message.title }}
</button>
} @else {
<button type="button">🔔 Nessuna notifica</button>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotificationBellComponent {
private readonly notifications = inject(NotificationsService);
readonly lastMessage = toSignal(this.notifications.messages$, {
initialValue: null as NotificationMessage | null
});
}
RxJS verarbeitet den Stream. Signal stellt der Benutzeroberfläche den neuesten Wert zur Verfügung. Dies ist die gesündeste Kombination.
Interoperabilität: toSignal und toObservable
Angular bietet Tools, mit denen Signals und RxJS miteinander kommunizieren können. Am häufigsten sind toSignal und toObservable.
toSignal wandelt ein Observable in ein Signal um.
readonly user = toSignal(this.authService.user$, {
initialValue: null
});
toObservable wandelt ein Signal in Observable um.
import { signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
readonly query = signal('');
private readonly query$ = toObservable(this.query);
readonly results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((query) => this.api.searchProducts(query))
),
{ initialValue: [] as Product[] }
);
Diese Technik ist sehr nützlich, wenn die Benutzereingaben als Signale verarbeitet werden, die Suche jedoch RxJS-Operatoren verwenden muss.
Häufige Fehler, die es zu vermeiden gilt
1. Ersetzen Sie RxJS überall, nur weil Signale
vorhanden sindSignale löschen RxJS nicht. Angular verwendet weiterhin Observable in vielen APIs. Wenn Sie versuchen, jeden Stream durch ein Signal zu ersetzen, riskieren Sie, die leistungsstärksten Operatoren von RxJS zu verlieren.
2. Verwenden Sie RxJS für jeden kleinen lokalen Staat
Ein boolescher Wert wie isMenuOpen, eine aktive Registerkarte oder ein lokaler Filter benötigen kein BehaviorSubject. Ein Signal ist besser lesbar.
3. Führen Sie HTTP-Aufrufe direkt aus, ohne sie zu überprüfen
effect ist nützlich für Nebeneffekte wie die Synchronisierung von localStorage, Seitentitel oder Protokoll. Es sollte kein Ort werden, an dem komplexe asynchrone Logik versteckt werden kann.
// Da evitare in scenari complessi:
effect(() => {
const query = this.query();
this.api.searchProducts(query).subscribe();
});
Für Entprellung, Bereinigung und Fehlerbehandlung ist eine RxJS-Pipeline sauberer.
4. Es werden zu viele nutzlose Berechnungen erstellt
computed ist leistungsstark, muss aber nicht jede Methode oder jeden kleinen Ausdruck ersetzen. Verwenden Sie diese Option, wenn der abgeleitete Wert wichtig ist, wiederverwendet wird oder die Berechnung kostspielig ist.
5. Ohne Muster hin und her konvertieren
Wenn Sie ein Observable nur in der Vorlage verwenden, kann die Pipe async ausreichend sein. Verwenden Sie toSignal, wenn Sie diesen Wert wirklich in das Signaldiagramm integrieren möchten.
Praktischer Entscheidungsbaum
- Ist dies ein lokaler Zustand der Komponente? Verwenden Sie Signale.
- Ist das ein Wert, der von anderen Staaten abgeleitet wurde? Verwenden Sie
berechnet. - Ist es ein zeitlicher Fluss von Ereignissen? Verwenden Sie RxJS.
- Benötigen Sie eine Entprellung? Verwenden Sie RxJS.
- Müssen Sie frühere Anfragen löschen? Verwenden Sie RxJS mit
switchMap. - Müssen Sie den letzten Wert in der Vorlage anzeigen? Betrachten Sie
toSignal. - Ist dies eine bereits geladene Liste, die lokal gefiltert werden soll? Signale.
- Ist dies eine Remote-Suche, während der Benutzer schreibt? RxJS.
- Ist das ein Warenkorb mit abgeleiteten Summen? Signale.
- Ist das ein WebSocket? RxJS.
- Ist das ein Formular mit asynchroner Validierung? RxJS plus Signale, wenn Sie den UI. -Status benötigen
- Ist es ein einfacher Feature Store? Signale.
- Ist die Orchestrierung mehrerer HTTP-Aufrufe abhängig? RxJS.
Empfohlene Architektur in einer echten App
In einem professionellen Angular-Projekt besteht eine gute Strategie darin, die Logik in eine Datenzugriffsebene zu unterteilen. Komponenten sollten nicht zu viel über HTTP, Caching, Wiederholungsversuche oder Datenzuordnung wissen.
src/app/features/products/
products.routes.ts
pages/
products-page.component.ts
product-detail-page.component.ts
components/
product-card.component.ts
product-filters.component.ts
data-access/
products-api.service.ts
products-store.service.ts
models/
product.model.ts
In der API-Servicedatei können Sie RxJS verwenden, da Angular HttpClient Observable.
Im Store können Sie das Ergebnis in Signalen speichern, berechnete Filter und Summen erstellen und der Komponente einfache Methoden zur Verfügung stellen.
In der Komponente können Sie Signale in der Vorlage lesen, async Pipe verwenden, wenn Sie direkt mit Observable arbeiten, oder mit toSignal konvertieren, wenn es die Lesbarkeit verbessert.
Signale und RxJS schließen sich nicht gegenseitig aus. Signale erleichtern Angular für den lokalen Zustand, den abgeleiteten Zustand und die Benutzeroberfläche. RxJS bleibt von entscheidender Bedeutung für asynchrone Streams, Streams, Löschvorgänge, Wiederholungsversuche, Entprellen und erweiterte Komposition.
Die beste Wahl ist, das richtige Werkzeug am richtigen Ort zu verwenden. Wenn Sie einen aktuellen Wert darstellen, wählen Sie Signale. Wenn Sie Ereignisse im Zeitverlauf modellieren, wählen Sie RxJS. Wenn Sie beides benötigen, kombinieren Sie sie ohne Gewalt.