Skip to content

Veelvoorkomende Fouten en Oplossingen

Deze pagina legt in detail 15 anti-patronen uit die vaak worden gezien bij het gebruik van RxJS met TypeScript, samen met hun oplossingen.

Inhoudsopgave

  1. Subject Extern Blootgesteld
  2. Geneste subscribe (Callback Hell)
  3. unsubscribe Vergeten (Geheugenlek)
  4. shareReplay Misbruik
  5. Bijwerkingen in map
  6. Cold/Hot Observable Verschil Negeren
  7. Promise en Observable Ongepast Vermengen
  8. Backpressure Negeren
  9. Fouten Onderdrukken
  10. DOM Event Subscription Lek
  11. Gebrek aan Type-veiligheid (Overmatig Gebruik van any)
  12. Onjuiste Operator Selectie
  13. Overmatige Complexiteit
  14. Status Wijziging in subscribe
  15. Gebrek aan Tests

1. Subject Extern Blootgesteld

Probleem

Als je een Subject direct blootstelt, kan externe code next() aanroepen, waardoor statusbeheer onvoorspelbaar wordt.

❌ Slecht Voorbeeld

ts
import { Subject } from 'rxjs';

// Subject direct exporteren
export const cartChanged$ = new Subject<void>();

// Iedereen kan next() aanroepen vanuit een ander bestand
cartChanged$.next(); // Kan op onverwachte momenten worden aangeroepen

✅ Goed Voorbeeld

ts
import { BehaviorSubject, Observable } from 'rxjs';

class CartStore {
  private readonly _items$ = new BehaviorSubject<string[]>([]);

  // Publiceren als alleen-lezen Observable
  readonly items$: Observable<string[]> = this._items$.asObservable();

  // Statuswijzigingen beheren via toegewijde methoden
  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();

Uitleg

  • Converteer naar alleen-lezen Observable met asObservable()
  • Maak statuswijzigingen alleen mogelijk via toegewijde methoden
  • Verbetert traceerbaarheid van wijzigingen en maakt debuggen eenvoudiger

2. Geneste subscribe (Callback Hell)

Probleem

Als je nog een subscribe aanroept binnen een subscribe, val je in callback hell en worden foutafhandeling en annuleringsverwerking complex.

❌ Slecht Voorbeeld

ts
import { of } from 'rxjs';

// Simulatie van API aanroepen
function apiA() {
  return of({ id: 1 });
}

function apiB(id: number) {
  return of({ id, token: 'abc123' });
}

function apiC(token: string) {
  return of({ success: true });
}

// Geneste subscribe
apiA().subscribe(a => {
  apiB(a.id).subscribe(b => {
    apiC(b.token).subscribe(result => {
      console.log('klaar', result);
    });
  });
});

✅ Goed Voorbeeld

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 });
};


// Afvlakken met higher-order operators
apiA().pipe(
  switchMap(a => apiB(a.id)),
  switchMap(b => apiC(b.token))
).subscribe(result => {
  console.log('klaar', result);
});

Uitleg

  • Gebruik higher-order operators zoals switchMap, mergeMap, concatMap
  • Foutafhandeling mogelijk op één plaats
  • Opzeggen van abonnement gebeurt ook in één keer
  • Verbeterde leesbaarheid van code

3. unsubscribe Vergeten (Geheugenlek)

Probleem

Als je abonnementen op oneindige streams (zoals event listeners) niet opzegt, ontstaan geheugenlekken.

❌ Slecht Voorbeeld

ts
import { fromEvent } from 'rxjs';

// Bij initialisatie van component
function setupResizeHandler() {
  fromEvent(window, 'resize').subscribe(() => {
    console.log('vergroot/verkleind');
  });
  // Abonnement niet opgezegd!
}

// Event listener blijft bestaan zelfs als component wordt vernietigd

✅ Goed Voorbeeld

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('opruimen'))
    ).subscribe(() => {
      console.log('vergroot/verkleind');
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

✅ Ander Goed Voorbeeld (Gebruik van Subscription)

ts
import { fromEvent, Subscription } from 'rxjs';

class MyComponent {
  private subscription = new Subscription();

  ngOnInit(): void {
    this.subscription.add(
      fromEvent(window, 'resize').subscribe(() => {
        console.log('vergroot/verkleind');
      })
    );
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

Uitleg

  • takeUntil patroon wordt aanbevolen (declaratief en duidelijk)
  • Handmatig beheer met Subscription is ook effectief
  • Voer altijd opzeggen van abonnement uit bij vernietiging van component

4. shareReplay Misbruik

Probleem

Bij gebruik van shareReplay zonder het gedrag te begrijpen, kunnen oude data worden gereproduceerd of geheugenlekken ontstaan.

❌ Slecht Voorbeeld

ts
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';

// Onbeperkte buffergrootte instellen
const shared$ = interval(1000).pipe(
  shareReplay() // Standaard is onbeperkte buffer
);

// Waarden blijven in geheugen zelfs als er geen abonnees meer zijn

✅ Goed Voorbeeld

ts
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';

// Buffergrootte en reference counting expliciet specificeren
const shared$ = interval(1000).pipe(
  take(10),
  shareReplay({
    bufferSize: 1,
    refCount: true // Geef resources vrij als er geen abonnees meer zijn
  })
);

Uitleg

  • Specificeer bufferSize expliciet (meestal 1)
  • Met refCount: true automatisch vrijgeven als er geen abonnees meer zijn
  • Voor streams die voltooien zoals HTTP requests is shareReplay({ bufferSize: 1, refCount: true }) veilig

5. Bijwerkingen in map

Probleem

Status wijzigen binnen de map operator veroorzaakt onvoorspelbaar gedrag.

❌ Slecht Voorbeeld

ts
import { of } from 'rxjs';
import { map } from 'rxjs';

let counter = 0;

const source$ = of(1, 2, 3).pipe(
  map(value => {
    counter++; // Bijwerking!
    return value * 2;
  })
);

source$.subscribe(console.log);
source$.subscribe(console.log); // counter neemt onverwacht toe

✅ Goed Voorbeeld

ts
import { of } from 'rxjs';
import { map, tap, scan } from 'rxjs';

// Alleen pure transformatie
const source$ = of(1, 2, 3).pipe(
  map(value => value * 2)
);

// Schei bijwerkingen af met tap
const withLogging$ = source$.pipe(
  tap(value => console.log('Verwerken:', value))
);

// Gebruik scan voor accumulatie van status
const withCounter$ = of(1, 2, 3).pipe(
  scan((acc, value) => ({ count: acc.count + 1, value }), { count: 0, value: 0 })
);

Uitleg

  • Gebruik map als pure functie
  • Schei bijwerkingen (logs, API aanroepen, etc.) af in tap
  • Gebruik scan of reduce voor statusaccumulatie

6. Cold/Hot Observable Verschil Negeren

Probleem

Gebruiken zonder te begrijpen of een Observable Cold of Hot is, veroorzaakt dubbele uitvoering of onverwacht gedrag.

❌ Slecht Voorbeeld

ts
import { ajax } from 'rxjs/ajax';

// Cold Observable - HTTP request wordt uitgevoerd bij elk abonnement
const data$ = ajax.getJSON('https://api.example.com/data');

data$.subscribe(console.log); // Request 1
data$.subscribe(console.log); // Request 2 (onnodige duplicatie)

✅ Goed Voorbeeld

ts
import { ajax } from 'rxjs/ajax';
import { shareReplay } from 'rxjs';

// Converteer naar Hot Observable en deel
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); // Gebruikt gecachete resultaat

Uitleg

  • Cold Observable: Wordt uitgevoerd bij elk abonnement (of, from, fromEvent, ajax, etc.)
  • Hot Observable: Wordt uitgevoerd onafhankelijk van abonnementen (Subject, gemulticast Observable, etc.)
  • Converteer Cold naar Hot met share / shareReplay

7. Promise en Observable Ongepast Vermengen

Probleem

Promise en Observable zonder correcte conversie vermengen resulteert in onvolledige foutafhandeling of annuleringsverwerking.

❌ Slecht Voorbeeld

ts
import { from } from 'rxjs';

async function fetchData(): Promise<string> {
  return 'data';
}

// Promise direct gebruiken
from(fetchData()).subscribe(data => {
  fetchData().then(moreData => { // Geneste Promise
    console.log(data, moreData);
  });
});

✅ Goed Voorbeeld

ts
import { from } from 'rxjs';
import { switchMap } from 'rxjs';

async function fetchData(): Promise<string> {
  return 'data';
}

// Promise converteren naar Observable voor uniforme verwerking
from(fetchData()).pipe(
  switchMap(() => from(fetchData()))
).subscribe(moreData => {
  console.log(moreData);
});

Uitleg

  • Converteer Promise naar Observable met from
  • Verwerk uniform binnen Observable pipeline
  • Foutafhandeling en annulering worden eenvoudiger

8. Backpressure Negeren

Probleem

Events met hoge frequentie verwerken zonder controle vermindert de prestaties.

❌ Slecht Voorbeeld

ts
import { fromEvent } from 'rxjs';

// Invoer events direct verwerken
fromEvent(document.getElementById('search'), 'input').subscribe(event => {
  // API aanroep bij elke invoer (overbelasting)
  searchAPI((event.target as HTMLInputElement).value);
});

function searchAPI(query: string): void {
  console.log('Zoeken naar:', query);
}

✅ Goed Voorbeeld

ts
import { fromEvent } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs';

// Debounce en annulering toepassen
fromEvent(document.getElementById('search'), 'input').pipe(
  map(event => (event.target as HTMLInputElement).value),
  debounceTime(300), // 300ms wachten
  distinctUntilChanged(), // Alleen wanneer waarde verandert
  switchMap(query => searchAPI(query)) // Annuleer oude requests
).subscribe(results => {
  console.log('Resultaten:', results);
});

Uitleg

  • Wacht een bepaalde tijd met debounceTime
  • Beperk maximale frequentie met throttleTime
  • Elimineer duplicaten met distinctUntilChanged
  • Annuleer oude requests met switchMap

9. Fouten Onderdrukken

Probleem

Fouten niet correct afhandelen maakt debuggen moeilijk en vermindert de gebruikerservaring.

❌ Slecht Voorbeeld

ts
import { ajax } from 'rxjs/ajax';
import { catchError } from 'rxjs';
import { of } from 'rxjs';

// Fouten negeren
ajax.getJSON('https://api.example.com/data').pipe(
  catchError(() => of(null)) // Foutinformatie gaat verloren
).subscribe(data => {
  console.log(data); // null komt aan maar oorzaak onbekend
});

✅ Goed Voorbeeld

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 => {
    console.error('API Fout:', error);
    // Informeer gebruiker
    showErrorToast('Ophalen van data is mislukt');
    // Retourneer alternatieve waarde met foutinformatie
    return of({ data: null, error: error.message } as ApiResponse);
  })
).subscribe((response) => {
  if (response.error) {
    console.log('Fallback modus vanwege:', response.error);
  }
});

function showErrorToast(message: string): void {
  console.log('Toast:', message);
}

Uitleg

  • Log fouten
  • Geef feedback aan gebruikers
  • Retourneer alternatieve waarde met foutinformatie
  • Overweeg retry strategie (retry, retryWhen)

10. DOM Event Subscription Lek

Probleem

DOM event listeners niet correct vrijgeven veroorzaakt geheugenlekken.

❌ Slecht Voorbeeld

ts
import { fromEvent } from 'rxjs';

class Widget {
  private button: HTMLButtonElement;

  constructor() {
    this.button = document.createElement('button');

    // Event listener registreren
    fromEvent(this.button, 'click').subscribe(() => {
      console.log('geklikt');
    });

    // Abonnement niet opgezegd
  }

  destroy(): void {
    this.button.remove();
    // Listener blijft bestaan
  }
}

✅ Goed Voorbeeld

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('geklikt');
    });
  }

  destroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    this.button.remove();
  }
}

Uitleg

  • Zeg abonnement betrouwbaar op met takeUntil patroon
  • Activeer destroy$ bij vernietiging van component
  • Geef listener vrij voor verwijderen van DOM element

11. Gebrek aan Type-veiligheid (Overmatig Gebruik van any)

Probleem

Overmatig gebruik van any schakelt TypeScript type checking uit en maakt runtime fouten waarschijnlijker.

❌ Slecht Voorbeeld

ts
import { Observable } from 'rxjs';
import { map } from 'rxjs';

function fetchUser(): Observable<any> {
  return new Observable(subscriber => {
    subscriber.next({ name: 'John', age: 30 });
  });
}

// Type checking werkt niet
fetchUser().pipe(
  map(user => user.naem) // Typfout! Niet opgemerkt tot runtime
).subscribe(console.log);

✅ Goed Voorbeeld

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 });
  });
}

// Type checking werkt
fetchUser().pipe(
  map(user => user.name) // Fout detectie tijdens compilatie
).subscribe(console.log);

Uitleg

  • Definieer interfaces of type aliases
  • Specificeer Observable<T> type parameters
  • Maak maximaal gebruik van TypeScript type inferentie

12. Onjuiste Operator Selectie

Probleem

Operators gebruiken die niet passen bij het doel is inefficiënt of veroorzaakt onverwacht gedrag.

❌ Slecht Voorbeeld

ts
import { fromEvent } from 'rxjs';
import { mergeMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';

// Zoeken bij elke button klik (oude requests worden niet geannuleerd)
fromEvent(document.getElementById('search-btn'), 'click').pipe(
  mergeMap(() => ajax.getJSON('https://api.example.com/search'))
).subscribe(console.log);

✅ Goed Voorbeeld

ts
import { fromEvent } from 'rxjs';
import { switchMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';

// Alleen laatste request verwerken (oude requests automatisch annuleren)
fromEvent(document.getElementById('search-btn'), 'click').pipe(
  switchMap(() => ajax.getJSON('https://api.example.com/search'))
).subscribe(console.log);

Gebruik van Belangrijke Higher-order Operators

OperatorToepassing
switchMapAlleen laatste stream verwerken (zoeken, autocomplete)
mergeMapParallelle verwerking (volgorde onbelangrijk)
concatMapSequentiële verwerking (volgorde belangrijk)
exhaustMapNegeer nieuwe input tijdens uitvoering (voorkom dubbelklik)

Uitleg

13. Overmatige Complexiteit

Probleem

Gevallen waarin eenvoudig te schrijven verwerking onnodig complex wordt gemaakt met RxJS.

❌ Slecht Voorbeeld

ts
import { Observable, of } from 'rxjs';
import { map, mergeMap, toArray } from 'rxjs';

// Eenvoudige array transformatie complex maken met RxJS
function doubleNumbers(numbers: number[]): Observable<number[]> {
  return of(numbers).pipe(
    mergeMap(arr => of(...arr)),
    map(n => n * 2),
    toArray()
  );
}

✅ Goed Voorbeeld

ts
import { fromEvent } from 'rxjs';
import { map } from 'rxjs';

// Array verwerking is voldoende met gewone JavaScript
function doubleNumbers(numbers: number[]): number[] {
  return numbers.map(n => n * 2);
}

// Gebruik RxJS voor asynchrone, event-gedreven verwerking
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));

Uitleg

  • Gebruik RxJS voor asynchrone verwerking of event streams
  • Synchrone array verwerking is voldoende met gewone JavaScript
  • Overweeg de balans tussen complexiteit en voordelen

14. Status Wijziging in subscribe

Probleem

Status direct wijzigen binnen subscribe maakt testen moeilijk en veroorzaakt bugs.

❌ Slecht Voorbeeld

ts
import { interval } from 'rxjs';

class Counter {
  count = 0;

  start(): void {
    interval(1000).subscribe(() => {
      this.count++; // Status wijzigen in subscribe
      this.updateUI();
    });
  }

  updateUI(): void {
    console.log('Aantal:', this.count);
  }
}

✅ Goed Voorbeeld

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 abonneert zich op count$
    this.count$.subscribe(count => this.updateUI(count));
  }

  updateUI(count: number): void {
    console.log('Aantal:', count);
  }
}

Uitleg

  • Beheer status met BehaviorSubject of scan
  • Gebruik subscribe als trigger
  • Testbaar en reactief ontwerp

15. Gebrek aan Tests

Probleem

RxJS code zonder tests naar productieomgeving deployen maakt regressie waarschijnlijker.

❌ Slecht Voorbeeld

ts
import { interval } from 'rxjs';
import { map, filter } from 'rxjs';

// Deployen zonder tests
export function getEvenNumbers() {
  return interval(1000).pipe(
    filter(n => n % 2 === 0),
    map(n => n * 2)
  );
}

✅ Goed Voorbeeld

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);
    });
  });
});

Uitleg

  • Voer marble tests uit met TestScheduler
  • Maak asynchrone verwerking synchroon testbaar
  • Zie Testmethoden voor details

Samenvatting

Door deze 15 anti-patronen te begrijpen en te vermijden, kun je robuustere en meer onderhoudbare RxJS code schrijven.

Referenties

Deze anti-patronen collectie is gemaakt met referentie aan de volgende betrouwbare bronnen.

Officiële Documentatie en Repositories

Anti-patronen en Best Practices

Aanvullende Bronnen

Gebruik voor Code Review

Controleer of je code anti-patronen bevat.

👉 Anti-patroon Preventie Checklist - Evalueer code met 15 checkpunten

Je kunt direct naar details van het corresponderende anti-patroon op deze pagina springen vanuit elk checkpunt.

Volgende Stappen

Integreer deze best practices in je dagelijkse codering en schrijf hoogwaardige RxJS code!

Uitgebracht onder de CC-BY-4.0 licentie.