Häufige Fehler und Gegenmaßnahmen
Auf dieser Seite werden 15 häufige Anti-Patterns bei der Verwendung von RxJS mit TypeScript und ihre jeweiligen Lösungen ausführlich erklärt.
Inhaltsverzeichnis
- Öffentliche Veröffentlichung von Subjects
- Verschachtelte subscribe (Callback-Hölle)
- Vergessenes unsubscribe (Speicherleck)
- Missbrauch von shareReplay
- Nebenwirkungen in map
- Ignorieren des Unterschieds zwischen Cold/Hot Observable
- Unangemessene Vermischung von Promise und Observable
- Ignorieren von Backpressure
- Unterdrückung von Fehlern
- Lecks bei DOM-Event-Subscriptions
- Mangel an Typsicherheit (übermäßige Verwendung von any)
- Ungeeignete Operator-Auswahl
- Übermäßige Komplexität
- Zustandsänderung innerhalb von subscribe
- Fehlen von Tests
1. Öffentliche Veröffentlichung von Subjects
Problem
Wenn Subject direkt veröffentlicht wird, kann next() von außen aufgerufen werden, was die Zustandsverwaltung unvorhersehbar macht.
❌ Schlechtes Beispiel
import { Subject } from 'rxjs';
// Subject direkt exportieren
export const cartChanged$ = new Subject<void>();
// Jeder kann next() von einer anderen Datei aus aufrufen
cartChanged$.next(); // Kann zu unerwarteten Zeitpunkten aufgerufen werden✅ Gutes Beispiel
import { BehaviorSubject, Observable } from 'rxjs';
class CartStore {
private readonly _items$ = new BehaviorSubject<string[]>([]);
// Als schreibgeschütztes Observable veröffentlichen
readonly items$: Observable<string[]> = this._items$.asObservable();
// Zustandsänderungen nur über dedizierte Methoden steuern
add(item: string): void {
this._items$.next([...this._items$.value, item]);
}
remove(item: string): void {
this._items$.next(
this._items$.value.filter(i => i !== item)
);
}
}
export const cartStore = new CartStore();Erklärung
- Mit
asObservable()in ein schreibgeschütztesObservablekonvertieren - Zustandsänderungen nur über dedizierte Methoden möglich machen
- Verbessert die Nachverfolgbarkeit von Änderungen und erleichtert das Debugging
2. Verschachtelte subscribe (Callback-Hölle)
Problem
Wenn subscribe innerhalb von subscribe aufgerufen wird, führt dies zur Callback-Hölle und macht Fehlerbehandlung und Abbruchverarbeitung komplex.
❌ Schlechtes Beispiel
import { of } from 'rxjs';
// API-Aufruf-Simulation
function apiA() {
return of({ id: 1 });
}
function apiB(id: number) {
return of({ id, token: 'abc123' });
}
function apiC(token: string) {
return of({ success: true });
}
// Verschachtelte subscribe
apiA().subscribe(a => {
apiB(a.id).subscribe(b => {
apiC(b.token).subscribe(result => {
console.log('done', result);
});
});
});✅ Gutes Beispiel
import { of } from 'rxjs';
import { switchMap } from 'rxjs';
function apiA() {
return of({ id: 1 });
}
function apiB(id: number) {
return of({ id, token: 'abc123' });
}
function apiC(token: string) {
return of({ success: true });
};
// Mit höheren Operatoren flach machen
apiA().pipe(
switchMap(a => apiB(a.id)),
switchMap(b => apiC(b.token))
).subscribe(result => {
console.log('done', result);
});Erklärung
- Verwendung von höheren Operatoren wie
switchMap,mergeMap,concatMap,exhaustMap - Fehlerbehandlung an einem Ort möglich
- Abmeldung nur einmal erforderlich
- Verbesserte Code-Lesbarkeit
3. Vergessenes unsubscribe (Speicherleck)
Problem
Wenn unendliche Streams (wie Event-Listener) nicht abgemeldet werden, tritt ein Speicherleck auf.
❌ Schlechtes Beispiel
import { fromEvent } from 'rxjs';
// Bei der Initialisierung einer Komponente
function setupResizeHandler() {
fromEvent(window, 'resize').subscribe(() => {
console.log('resized');
});
// Subscription wird nicht abgemeldet!
}
// Event-Listener bleibt auch nach Zerstörung der Komponente bestehen✅ Gutes Beispiel
import { fromEvent, Subject } from 'rxjs';
import { takeUntil, finalize } from 'rxjs';
class MyComponent {
private readonly destroy$ = new Subject<void>();
ngOnInit(): void {
fromEvent(window, 'resize').pipe(
takeUntil(this.destroy$),
finalize(() => console.log('cleanup'))
).subscribe(() => {
console.log('resized');
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}✅ Weiteres gutes Beispiel (mit Subscription)
import { fromEvent, Subscription } from 'rxjs';
class MyComponent {
private subscription = new Subscription();
ngOnInit(): void {
this.subscription.add(
fromEvent(window, 'resize').subscribe(() => {
console.log('resized');
})
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}Erklärung
- Das
takeUntil-Pattern wird empfohlen (deklarativ und klar) - Manuelle Verwaltung mit
Subscriptionist ebenfalls wirksam - Immer Abmeldung bei Zerstörung der Komponente durchführen
4. Missbrauch von shareReplay
Problem
Wenn shareReplay ohne Verständnis seiner Funktionsweise verwendet wird, können alte Daten wiedergegeben werden oder Speicherlecks auftreten.
❌ Schlechtes Beispiel
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';
// Unbegrenzte Puffergröße setzen
const shared$ = interval(1000).pipe(
shareReplay() // Standard ist unbegrenzter Puffer
);
// Werte bleiben im Speicher, auch wenn keine Abonnenten mehr vorhanden sind✅ Gutes Beispiel
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';
// Puffergröße und Referenzzählung explizit angeben
const shared$ = interval(1000).pipe(
take(10),
shareReplay({
bufferSize: 1,
refCount: true // Ressourcen freigeben, wenn keine Abonnenten mehr vorhanden sind
})
);Erklärung
bufferSizeexplizit angeben (normalerweise 1)refCount: truefür automatische Freigabe bei fehlenden Abonnenten- Für HTTP-Requests und andere abgeschlossene Streams ist
shareReplay({ bufferSize: 1, refCount: true })sicher
5. Nebenwirkungen in map
Problem
Wenn Zustand innerhalb des map-Operators geändert wird, führt dies zu unvorhersehbarem Verhalten.
❌ Schlechtes Beispiel
import { of } from 'rxjs';
import { map } from 'rxjs';
let counter = 0;
const source$ = of(1, 2, 3).pipe(
map(value => {
counter++; // Nebenwirkung!
return value * 2;
})
);
source$.subscribe(console.log);
source$.subscribe(console.log); // counter erhöht sich unerwartet✅ Gutes Beispiel
import { of } from 'rxjs';
import { map, tap, scan } from 'rxjs';
// Nur reine Transformation
const source$ = of(1, 2, 3).pipe(
map(value => value * 2)
);
// Nebenwirkungen mit tap trennen
const withLogging$ = source$.pipe(
tap(value => console.log('Processing:', value))
);
// Zustandsakkumulation mit scan
const withCounter$ = of(1, 2, 3).pipe(
scan((acc, value) => ({ count: acc.count + 1, value }), { count: 0, value: 0 })
);Erklärung
mapals reine Funktion verwenden- Nebenwirkungen (Logging, API-Aufrufe usw.) in
taptrennen - Zustandsakkumulation mit
scanoderreduce
6. Ignorieren des Unterschieds zwischen Cold/Hot Observable
Problem
Wenn ohne Verständnis verwendet wird, ob ein Observable Cold oder Hot ist, führt dies zu doppelter Ausführung oder unerwartetem Verhalten.
❌ Schlechtes Beispiel
import { ajax } from 'rxjs/ajax';
// Cold Observable - HTTP-Request wird bei jeder Subscription ausgeführt
const data$ = ajax.getJSON('https://api.example.com/data');
data$.subscribe(console.log); // Request 1
data$.subscribe(console.log); // Request 2 (unnötige Duplizierung)✅ Gutes Beispiel
import { ajax } from 'rxjs/ajax';
import { shareReplay } from 'rxjs';
// In Hot Observable konvertieren und teilen
const data$ = ajax.getJSON('https://api.example.com/data').pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
data$.subscribe(console.log); // Request 1
data$.subscribe(console.log); // Verwendet gecachtes ErgebnisErklärung
- Cold Observable: Wird bei jeder Subscription ausgeführt (
of,from,fromEvent,ajaxusw.) - Hot Observable: Wird unabhängig von Subscriptions ausgeführt (
Subject, multicast-Observables usw.) - Mit
share/shareReplaykann Cold zu Hot konvertiert werden
7. Unangemessene Vermischung von Promise und Observable
Problem
Wenn Promise und Observable nicht ordnungsgemäß konvertiert und vermischt werden, wird die Fehlerbehandlung und Abbruchverarbeitung unvollständig.
❌ Schlechtes Beispiel
import { from } from 'rxjs';
async function fetchData(): Promise<string> {
return 'data';
}
// Promise direkt verwendet
from(fetchData()).subscribe(data => {
fetchData().then(moreData => { // Verschachtelte Promise
console.log(data, moreData);
});
});✅ Gutes Beispiel
import { from } from 'rxjs';
import { switchMap } from 'rxjs';
async function fetchData(): Promise<string> {
return 'data';
}
// Promise zu Observable konvertieren und vereinheitlichen
from(fetchData()).pipe(
switchMap(() => from(fetchData()))
).subscribe(moreData => {
console.log(moreData);
});Erklärung
- Promise mit
fromzu Observable konvertieren - Einheitliche Verarbeitung innerhalb der Observable-Pipeline
- Fehlerbehandlung und Abbruch werden einfach
8. Ignorieren von Backpressure
Problem
Wenn hochfrequente Events ohne Kontrolle verarbeitet werden, sinkt die Leistung.
❌ Schlechtes Beispiel
import { fromEvent } from 'rxjs';
// Eingabeereignisse direkt verarbeiten
fromEvent(document.getElementById('search'), 'input').subscribe(event => {
// API-Aufruf bei jeder Eingabe (Überlast)
searchAPI((event.target as HTMLInputElement).value);
});
function searchAPI(query: string): void {
console.log('Searching for:', query);
}✅ Gutes Beispiel
import { fromEvent } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs';
// Debouncing und Abbruch anwenden
fromEvent(document.getElementById('search'), 'input').pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(300), // 300ms warten
distinctUntilChanged(), // Nur wenn sich Wert ändert
switchMap(query => searchAPI(query)) // Alte Requests abbrechen
).subscribe(results => {
console.log('Results:', results);
});Erklärung
- Mit
debounceTimeeine bestimmte Zeit warten - Mit
throttleTimemaximale Frequenz begrenzen - Mit
distinctUntilChangedDuplikate ausschließen - Mit
switchMapalte Requests abbrechen
9. Unterdrückung von Fehlern
Problem
Wenn Fehler nicht ordnungsgemäß behandelt werden, wird das Debugging schwierig und die Benutzererfahrung leidet.
❌ Schlechtes Beispiel
import { ajax } from 'rxjs/ajax';
import { catchError } from 'rxjs';
import { of } from 'rxjs';
// Fehler ignorieren
ajax.getJSON('https://api.example.com/data').pipe(
catchError(() => of(null)) // Fehlerinformationen gehen verloren
).subscribe(data => {
console.log(data); // null kommt an, Ursache unklar
});✅ Gutes Beispiel
import { ajax } from 'rxjs/ajax';
import { catchError } from 'rxjs';
import { of } from 'rxjs';
interface ApiResponse {
data: unknown;
error?: string;
}
ajax.getJSON<ApiResponse>('https://api.example.com/data').pipe(
catchError(error => {
console.error('API Error:', error);
// Benutzer benachrichtigen
showErrorToast('Fehler beim Abrufen der Daten');
// Alternativen Wert mit Fehlerinformationen zurückgeben
return of({ data: null, error: error.message } as ApiResponse);
})
).subscribe((response) => {
if (response.error) {
console.log('Fallback mode due to:', response.error);
}
});
function showErrorToast(message: string): void {
console.log('Toast:', message);
}Erklärung
- Fehler protokollieren
- Feedback an Benutzer geben
- Alternativen Wert mit Fehlerinformationen zurückgeben
- Retry-Strategie in Betracht ziehen (
retry,retryWhen)
10. Lecks bei DOM-Event-Subscriptions
Problem
Wenn DOM-Event-Listener nicht ordnungsgemäß freigegeben werden, tritt ein Speicherleck auf.
❌ Schlechtes Beispiel
import { fromEvent } from 'rxjs';
class Widget {
private button: HTMLButtonElement;
constructor() {
this.button = document.createElement('button');
// Event-Listener registrieren
fromEvent(this.button, 'click').subscribe(() => {
console.log('clicked');
});
// Keine Abmeldung
}
destroy(): void {
this.button.remove();
// Listener bleibt bestehen
}
}✅ Gutes Beispiel
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';
class Widget {
private button: HTMLButtonElement;
private readonly destroy$ = new Subject<void>();
constructor() {
this.button = document.createElement('button');
fromEvent(this.button, 'click').pipe(
takeUntil(this.destroy$)
).subscribe(() => {
console.log('clicked');
});
}
destroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.button.remove();
}
}Erklärung
- Zuverlässige Abmeldung mit dem
takeUntil-Pattern destroy$bei Zerstörung der Komponente auslösen- Listener vor Entfernung des DOM-Elements freigeben
11. Mangel an Typsicherheit (übermäßige Verwendung von any)
Problem
Übermäßige Verwendung von any deaktiviert die Typprüfung von TypeScript und erhöht die Wahrscheinlichkeit von Laufzeitfehlern.
❌ Schlechtes Beispiel
import { Observable } from 'rxjs';
import { map } from 'rxjs';
function fetchUser(): Observable<any> {
return new Observable(subscriber => {
subscriber.next({ name: 'John', age: 30 });
});
}
// Typprüfung funktioniert nicht
fetchUser().pipe(
map(user => user.naem) // Tippfehler! Wird erst zur Laufzeit bemerkt
).subscribe(console.log);✅ Gutes Beispiel
import { Observable } from 'rxjs';
import { map } from 'rxjs';
interface User {
name: string;
age: number;
}
function fetchUser(): Observable<User> {
return new Observable<User>(subscriber => {
subscriber.next({ name: 'John', age: 30 });
});
}
// Typprüfung funktioniert
fetchUser().pipe(
map(user => user.name) // Fehler wird zur Kompilierzeit erkannt
).subscribe(console.log);Erklärung
- Interfaces oder Typ-Aliase definieren
- Typparameter von
Observable<T>explizit angeben - TypeScript-Typinferenz maximal nutzen
12. Ungeeignete Operator-Auswahl
Problem
Die Verwendung ungeeigneter Operatoren führt zu Ineffizienz oder unerwartetem Verhalten.
❌ Schlechtes Beispiel
import { fromEvent } from 'rxjs';
import { mergeMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';
// Suche bei jedem Button-Klick (alte Requests werden nicht abgebrochen)
fromEvent(document.getElementById('search-btn'), 'click').pipe(
mergeMap(() => ajax.getJSON('https://api.example.com/search'))
).subscribe(console.log);✅ Gutes Beispiel
import { fromEvent } from 'rxjs';
import { switchMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';
// Nur neuesten Request verarbeiten (alte Requests werden automatisch abgebrochen)
fromEvent(document.getElementById('search-btn'), 'click').pipe(
switchMap(() => ajax.getJSON('https://api.example.com/search'))
).subscribe(console.log);Verwendung der wichtigsten höheren Operatoren
| Operator | Verwendung |
|---|---|
switchMap | Nur neuesten Stream verarbeiten (Suche, Autocomplete) |
mergeMap | Parallele Verarbeitung (Reihenfolge unwichtig) |
concatMap | Sequentielle Verarbeitung (Reihenfolge wichtig) |
exhaustMap | Neue Eingaben während der Ausführung ignorieren (Button-Klick-Prävention) |
Erklärung
- Verhalten jedes Operators verstehen
- Geeignete Auswahl je nach Anwendungsfall
- Details siehe Transformationsoperatoren
13. Übermäßige Komplexität
Problem
Einfache Prozesse werden mit RxJS übermäßig kompliziert gemacht.
❌ Schlechtes Beispiel
import { Observable, of } from 'rxjs';
import { map, mergeMap, toArray } from 'rxjs';
// Einfache Array-Transformation mit RxJS kompliziert machen
function doubleNumbers(numbers: number[]): Observable<number[]> {
return of(numbers).pipe(
mergeMap(arr => of(...arr)),
map(n => n * 2),
toArray()
);
}✅ Gutes Beispiel
import { fromEvent } from 'rxjs';
import { map } from 'rxjs';
// Array-Verarbeitung ist mit normalem JavaScript ausreichend
function doubleNumbers(numbers: number[]): number[] {
return numbers.map(n => n * 2);
}
// RxJS für asynchrone, ereignisgesteuerte Verarbeitung verwenden
const button = document.getElementById('calc-btn') as HTMLButtonElement;
const numbers = [1, 2, 3, 4, 5];
fromEvent(button, 'click').pipe(
map(() => doubleNumbers(numbers))
).subscribe(result => console.log(result));Erklärung
- RxJS für asynchrone Verarbeitung und Event-Streams verwenden
- Synchrone Array-Verarbeitung ist mit normalem JavaScript ausreichend
- Balance zwischen Komplexität und Nutzen berücksichtigen
14. Zustandsänderung innerhalb von subscribe
Problem
Wenn Zustand direkt innerhalb von subscribe geändert wird, wird das Testen schwierig und es wird zur Ursache von Bugs.
❌ Schlechtes Beispiel
import { interval } from 'rxjs';
class Counter {
count = 0;
start(): void {
interval(1000).subscribe(() => {
this.count++; // Zustandsänderung innerhalb von subscribe
this.updateUI();
});
}
updateUI(): void {
console.log('Count:', this.count);
}
}✅ Gutes Beispiel
import { interval, BehaviorSubject } from 'rxjs';
import { scan, tap } from 'rxjs';
class Counter {
private readonly count$ = new BehaviorSubject<number>(0);
start(): void {
interval(1000).pipe(
scan(acc => acc + 1, 0),
tap(count => this.count$.next(count))
).subscribe();
// UI abonniert count$
this.count$.subscribe(count => this.updateUI(count));
}
updateUI(count: number): void {
console.log('Count:', count);
}
}Erklärung
- Zustand mit
BehaviorSubjectoderscanverwalten subscribenur als Trigger verwenden- Testbares und reaktives Design
15. Fehlen von Tests
Problem
Wenn RxJS-Code ohne Tests in die Produktionsumgebung gebracht wird, treten leicht Regressionen auf.
❌ Schlechtes Beispiel
import { interval } from 'rxjs';
import { map, filter } from 'rxjs';
// Ohne Tests bereitstellen
export function getEvenNumbers() {
return interval(1000).pipe(
filter(n => n % 2 === 0),
map(n => n * 2)
);
}✅ Gutes Beispiel
import { TestScheduler } from 'rxjs/testing';
import { getEvenNumbers } from './numbers';
describe('getEvenNumbers', () => {
let scheduler: TestScheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should emit only even numbers doubled', () => {
scheduler.run(({ expectObservable }) => {
const expected = '1s 0 1s 4 1s 8';
expectObservable(getEvenNumbers()).toBe(expected);
});
});
});Erklärung
- Marble-Tests mit
TestSchedulerdurchführen - Asynchrone Verarbeitung kann synchron getestet werden
- Details siehe Testmethoden
Zusammenfassung
Durch das Verständnis und Vermeiden dieser 15 Anti-Patterns können Sie robusteren und wartbareren RxJS-Code schreiben.
Referenzen
Diese Anti-Pattern-Sammlung wurde unter Bezugnahme auf folgende vertrauenswürdige Quellen erstellt.
Offizielle Dokumentation und Repositories
- RxJS Offizielle Dokumentation - Offizielle Referenz für Operatoren und API
- GitHub Issue #5931 - Diskussion über shareReplay-Speicherleck-Problem
Anti-Patterns und Best Practices
- RxJS in Angular - Antipattern 1: Nested subscriptions - Thinktecture AG
- RxJS in Angular - Antipattern 2: Stateful Streams - Thinktecture AG
- RxJS Best Practices in Angular 16 (2025) - InfoQ (Mai 2025)
- RxJS: Why memory leaks occur when using a Subject - Angular In Depth
- RxJS Antipatterns - Brian Love
Zusätzliche Ressourcen
- Learn RxJS - Praktischer Leitfaden für Operatoren und Patterns
- RxJS Marbles - Visuelles Verständnis von Operatoren
Verwendung bei Code-Reviews
Überprüfen Sie, ob Ihr Code Anti-Patterns enthält.
👉 Anti-Pattern-Vermeidungs-Checkliste - Überprüfen Sie Ihren Code mit 15 Prüfpunkten
Von jedem Prüfpunkt aus können Sie direkt zu den Details des entsprechenden Anti-Patterns auf dieser Seite springen.
Nächste Schritte
- Fehlerbehandlung - Lernen Sie detailliertere Fehlerbehandlungsstrategien
- Testmethoden - Erwerben Sie effektive Testmethoden für RxJS-Code
- Verständnis von Operatoren - Lernen Sie die detaillierte Verwendung jedes Operators
Integrieren Sie diese Best Practices in Ihr tägliches Coding und schreiben Sie hochwertigen RxJS-Code!