Skip to content

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 ​

  1. Öffentliche Veröffentlichung von Subjects
  2. Verschachtelte subscribe (Callback-Hölle)
  3. Vergessenes unsubscribe (Speicherleck)
  4. Missbrauch von shareReplay
  5. Nebenwirkungen in map
  6. Ignorieren des Unterschieds zwischen Cold/Hot Observable
  7. Unangemessene Vermischung von Promise und Observable
  8. Ignorieren von Backpressure
  9. Unterdrückung von Fehlern
  10. Lecks bei DOM-Event-Subscriptions
  11. Mangel an Typsicherheit (übermäßige Verwendung von any)
  12. Ungeeignete Operator-Auswahl
  13. Übermäßige Komplexität
  14. Zustandsänderung innerhalb von subscribe
  15. 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 ​

ts
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 ​

ts
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ütztes Observable konvertieren
  • 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 ​

ts
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 ​

ts
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 ​

ts
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 ​

ts
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) ​

ts
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 Subscription ist 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 ​

ts
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 ​

ts
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 ​

  • bufferSize explizit angeben (normalerweise 1)
  • refCount: true fü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 ​

ts
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 ​

ts
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 ​

  • map als reine Funktion verwenden
  • Nebenwirkungen (Logging, API-Aufrufe usw.) in tap trennen
  • Zustandsakkumulation mit scan oder reduce

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 ​

ts
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 ​

ts
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 Ergebnis

Erklärung ​

  • Cold Observable: Wird bei jeder Subscription ausgeführt (of, from, fromEvent, ajax usw.)
  • Hot Observable: Wird unabhängig von Subscriptions ausgeführt (Subject, multicast-Observables usw.)
  • Mit share / shareReplay kann 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 ​

ts
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 ​

ts
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 from zu 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 ​

ts
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 ​

ts
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 debounceTime eine bestimmte Zeit warten
  • Mit throttleTime maximale Frequenz begrenzen
  • Mit distinctUntilChanged Duplikate ausschließen
  • Mit switchMap alte Requests abbrechen

9. Unterdrückung von Fehlern ​

Problem ​

Wenn Fehler nicht ordnungsgemäß behandelt werden, wird das Debugging schwierig und die Benutzererfahrung leidet.

❌ Schlechtes Beispiel ​

ts
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 ​

ts
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: unknown) => {
    console.error('API Error:', error);
    // Benutzer benachrichtigen
    showErrorToast('Fehler beim Abrufen der Daten');
    // Alternativen Wert mit Fehlerinformationen zurückgeben
    const message = error instanceof Error ? error.message : String(error);
    return of({ data: null, 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 ​

ts
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 ​

ts
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 ​

ts
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 ​

ts
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 ​

ts
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 ​

ts
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 ​

OperatorVerwendung
switchMapNur neuesten Stream verarbeiten (Suche, Autocomplete)
mergeMapParallele Verarbeitung (Reihenfolge unwichtig)
concatMapSequentielle Verarbeitung (Reihenfolge wichtig)
exhaustMapNeue Eingaben während der Ausführung ignorieren (Button-Klick-Prävention)

Erklärung ​

13. Übermäßige Komplexität ​

Problem ​

Einfache Prozesse werden mit RxJS übermäßig kompliziert gemacht.

❌ Schlechtes Beispiel ​

ts
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 ​

ts
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 ​

ts
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 ​

ts
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 BehaviorSubject oder scan verwalten
  • subscribe nur 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 ​

ts
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 ​

ts
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 TestScheduler durchfü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 ​

Anti-Patterns und Best Practices ​

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 ​

Integrieren Sie diese Best Practices in Ihr tägliches Coding und schreiben Sie hochwertigen RxJS-Code!

Veröffentlicht unter CC-BY-4.0-Lizenz.