Skip to content

Belemmeringen voor conceptueel begrip ​

De eerste barrière voor RxJS is het begrijpen van concepten. Vooral voor ontwikkelaars die gewend zijn aan Promise, kan het gedrag van Observable contra-intuïtief zijn.

Essentiële verschillen tussen Observable vs Promise ​

Promise: eenmalige asynchrone verwerking ​

typescript
// Promise: waarde slechts eenmaal retourneren
const userPromise = fetch('/api/user/1').then(res => res.json());

userPromise.then(user => console.log(user)); // informatie over gebruiker slechts eenmaal ophalen
userPromise.then(user => console.log(user)); // zelfde resultaat in cache opgeslagen

Kenmerken

  • Eager (onmiddellijke uitvoering) - verwerking begint op het moment dat de Promise wordt aangemaakt
  • Voltooit slechts eenmaal - slechts eenmaal, succes of mislukking
  • Niet annuleerbaar - eenmaal gestart, kan niet worden gestopt
  • Altijd Hot - meerdere then delen hetzelfde resultaat

Observable: Stream (gegevensstroom met tijdas) ​

typescript
import { Observable } from 'rxjs';

// Observable: meerdere waarden die langs een tijdas stromen
const user$ = new Observable<User>(subscriber => {
  console.log('Observable uitvoering gestart!');
  fetch('/api/user/1')
    .then(res => res.json())
    .then(user => {
      subscriber.next(user);
      subscriber.complete();
    });
});

// ❌ Er gebeurt nu nog niets (Lazy)
console.log('Observable creatie voltooid');

// ✅ Pas na subscribe wordt het uitgevoerd
user$.subscribe(user => console.log('Subscribe 1:', user));
user$.subscribe(user => console.log('Subscribe 2:', user));
// → API-aanroep wordt twee keer uitgevoerd (Cold Observable)

Uitvoer ​

Observable creatie voltooid
Observable uitvoering gestart!
Subscribe 1: { id: 1, name: 'Alice' }
Observable uitvoering gestart!
Subscribe 2: { id: 1, name: 'Alice' }

Kenmerken

  • Lazy (vertraagde uitvoering) - er gebeurt niets tot subscribe
  • Meerdere waarden kunnen worden doorgegeven - next() kan meerdere keren worden aangeroepen
  • Annuleerbaar - kan worden gestopt door unsubscribe
  • Cold of Hot - kan kiezen om per abonnement uit te voeren of te delen

Vergelijkingstabel ​

EigenschapPromiseObservable
UitvoertimingOnmiddellijk (Eager)Bij subscribe (Lazy)
Aantal waardenEenmalig0 of meer (meerdere)
AnnulerenNiet toegestaanMogelijk (unsubscribe)
HergebruikGecachte resultatenOpnieuw uitvoeren per abonnement (Cold)
Na foutBeëindigdBeëindigd (retry mogelijk)

Verschillen in gedrag visualiseren ​

Het volgende sequentiediagram toont het verschil in uitvoeringsstroom tussen Promise en Observable.

Vaak voorkomende misvattingen ​

❌ Misvatting 1: "Observable is alleen asynchroon" ​

typescript
// Observable kan ook synchrone verwerking aan
import { of } from 'rxjs';

const sync$ = of(1, 2, 3);

console.log('Before subscribe');
sync$.subscribe(value => console.log(value));
console.log('After subscribe');

// Uitvoer (synchroon uitgevoerd):
// Before subscribe
// 1
// 2
// 3
// After subscribe

❌ Misvatting 2: "subscribe retourneert een waarde" ​

typescript
import { map, of } from "rxjs";

const observable$ = of(1, 2, 3);

// ❌ Slecht voorbeeld: Promise-achtig denken
const value = observable$.subscribe(x => x); // value is Subscription-object
console.log(value); // Subscription { ... } ← niet de verwachte waarde

// ✅ Goed voorbeeld: Observable-achtig denken
observable$.pipe(
  map(x => x * 2)
).subscribe(value => {
  console.log(value); // waarde hier gebruiken
});

Intuïtief begrip van Cold vs Hot ​

Cold Observable: Onafhankelijke stromen per abonnement ​

typescript
import { interval } from 'rxjs';
import { take } from 'rxjs';

// Cold: elke abonnee heeft een onafhankelijke timer
const cold$ = interval(1000).pipe(take(3));

console.log('Abonnement 1 gestart');
cold$.subscribe(x => console.log('Abonnement 1:', x));

setTimeout(() => {
  console.log('Abonnement 2 gestart (2 seconden later)');
  cold$.subscribe(x => console.log('Abonnement 2:', x));
}, 2000);

// Uitvoer:
// Abonnement 1 gestart
// Abonnement 1: 0        (na 1 seconde)
// Abonnement 1: 1        (na 2 seconden)
// Abonnement 2 gestart (na 2 seconden)
// Abonnement 1: 2        (na 3 seconden)
// Abonnement 2: 0        (na 3 seconden) ← Abonnement 2 start vanaf het begin
// Abonnement 2: 1        (na 4 seconden)
// Abonnement 2: 2        (na 5 seconden)

Cold kenmerken

  • Onafhankelijke uitvoering per abonnement
  • Bewaart een "blauwdruk" van de gegevens
  • Voorbeelden: HTTP API-aanroepen, timers, lezen van bestanden

Hot Observable: Alle abonnees delen dezelfde stroom ​

typescript
import { interval } from 'rxjs';
import { take, share } from 'rxjs';

// Hot: gedeeld door share()
const hot$ = interval(1000).pipe(
  take(3),
  share() // dit maakt het Hot
);

console.log('Abonnement 1 gestart');
hot$.subscribe(x => console.log('Subscribe 1:', x));

setTimeout(() => {
  console.log('Abonnement 2 gestart (2 seconden later)');
  hot$.subscribe(x => console.log('Subscribe 2:', x));
}, 2000);

// Uitvoer:
// Abonnement 1 gestart
// Subscribe 1: 0        (na 1 seconde)
// Subscribe 1: 1        (na 2 seconden)
// Abonnement 2 gestart (2 seconden later)
// Subscribe 1: 2        (na 3 seconden)
// Subscribe 2: 2        (na 3 seconden) ← Abonnement 2 sluit zich halverwege aan

Hot kenmerken

  • Alle abonnees delen dezelfde uitvoering
  • Gegevens worden "uitgezonden"
  • Voorbeelden: Click Event, WebSocket, Subject

Hoe Cold/Hot identificeren ​

typescript
import { fromEvent, interval, of, Subject } from 'rxjs';
import { share } from 'rxjs';

// Cold (onafhankelijke uitvoering per abonnement)
const cold1$ = of(1, 2, 3);
const cold2$ = interval(1000);
const cold3$ = ajax('/api/data');
const cold4$ = fromEvent(button, 'click'); // Cold maar speciaal

// Hot (gedeeld onder abonnees)
const hot1$ = new Subject<number>();
const hot2$ = interval(1000).pipe(share()); // zet Cold om in Hot

Hoe ze uit elkaar te houden

  • Creatiefuncties (of, from, fromEvent, interval, ajax, enz.) → Cold
  • Subject-gebaseerd → Hot
  • share(), shareReplay() gebruiken → Cold omzetten in Hot

Verschuiving in denken naar declaratief programmeren ​

Imperatief vs. Declaratief ​

RxJS is een paradigma van declaratief programmeren.

❌ Imperatief denken (Promise/async-await) ​

typescript
// Imperatief: beschrijf "hoe" te verwerken
async function processUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();

  const activeUsers = [];
  for (const user of users) {
    if (user.isActive) {
      activeUsers.push(user);
    }
  }

  const userNames = [];
  for (const user of activeUsers) {
    userNames.push(user.name.toUpperCase());
  }

  return userNames;
}

✅ Declaratief denken (RxJS) ​

typescript
import { from } from 'rxjs';
import { mergeMap, filter, map, toArray } from 'rxjs';

// Declaratief: beschrijf "wat" te converteren
const processUsers$ = from(fetch('/api/users')).pipe(
  mergeMap(res => res.json()),
  mergeMap(users => users), // array uitbreiden
  filter(user => user.isActive),
  map(user => user.name.toUpperCase()),
  toArray()
);

processUsers$.subscribe(userNames => console.log(userNames));

Verschillen

  • Imperatief: Beschrijft een procedure (lus, voorwaardelijke vertakking, variabele toewijzing)
  • Declaratief: Beschrijft een pijplijn van conversies (gegevensstroom)

Belangrijkste punten van gedachtetransformatie ​

Punt 1: Geen gegevensverwerking binnen subscribe ​

Gegevenstransformatie moet binnen pipe worden gedaan en subscribe mag alleen neveneffecten hebben.

typescript
import { filter, map, of } from "rxjs";

const observable$ = of(1, 2, 3);
// ❌ Slecht voorbeeld: verwerking in subscribe
observable$.subscribe(value => {
  const doubled = value * 2;           // 👈 berekenen binnen subscribe
  const filtered = doubled > 4 ? doubled : null;  // 👈 voorwaardelijk in subscribe
  if (filtered) {                      // 👈 if statement binnen subscribe
    console.log(filtered);
  }
});

// ✅ Goed voorbeeld: conversie binnen pipe
observable$.pipe(
  map(value => value * 2),       // berekening wordt gedaan in pipe
  filter(value => value > 4)     // filteren gebeurt ook in pipe
).subscribe(value => console.log(value));  // subscribe is slechts een neveneffect

Punt 2: Gebruik geen tussenliggende variabelen ​

typescript
import { filter, map, Observable, of } from "rxjs";

const source$ = of(1, 2, 3, 4, 5);

// ❌ Slecht voorbeeld: opslaan in tussenliggende variabele
let doubled$: Observable<number>;      // 👈 Tussenvariabele declareren
let filtered$: Observable<number>;     // 👈 declareer tussenvariabele

doubled$ = source$.pipe(map(x => x * 2));    // 👈 toekennen aan tussenliggende variabele
filtered$ = doubled$.pipe(filter(x => x > 5)); // 👈 toekennen aan tussenliggende variabele
filtered$.subscribe(console.log);

// ✅ Goed voorbeeld: verbinden met pijplijn
source$.pipe(
  map(x => x * 2),      // pijplijn direct
  filter(x => x > 5)    // pijplijn direct
).subscribe(console.log);

Punt 3: Vermijd geneste subscribe ​

typescript
// ❌ Slecht voorbeeld: geneste subscribe
getUser$(userId).subscribe(user => {
  getOrders$(user.id).subscribe(orders => {  // 👈 meer subscribe in subscribe (genest)
    console.log(user, orders);
  });  // 👈 Uitschrijven wordt ingewikkelder
});

// ✅ Goed voorbeeld: afvlakken met mergeMap
getUser$(userId).pipe(
  mergeMap(user =>                // binnenste Observable afvlakken met mergeMap
    getOrders$(user.id).pipe(
      map(orders => ({ user, orders }))
    )
  )
).subscribe(({ user, orders }) => console.log(user, orders));  // slechts één subscribe locatie

Punt 4: Organiseer in 3-stappen-scheidingssyntaxis ​

Een belangrijke techniek die de leesbaarheid en onderhoudbaarheid van RxJS-code sterk verbetert, is de fasescheidingssyntaxis.

typescript
// ❌ Slecht voorbeeld: een one-liner met alles door elkaar
fromEvent(document, 'click').pipe(
  map(event => (event as MouseEvent).clientX),
  filter(x => x > 100),
  throttleTime(200)
).subscribe({
  next: x => console.log('Click positie:', x),
  error: err => console.error(err)
});

Problemen

  • Gemengde streamdefinities, conversies en abonnementen
  • Moeilijk te debuggen (niet zeker waar het probleem zich voordoet)
  • Moeilijk te testen
  • Niet herbruikbaar
typescript
// ✅ Goed voorbeeld: scheiding in 3 fasen

import { filter, fromEvent, map, throttleTime } from "rxjs";

// 1. Observable definitie (bron van stream)
const clicks$ = fromEvent(document, 'click');

// 2. Pijplijndefinitie (gegevensconversieproces)
const processed$ = clicks$.pipe(
  map(event => (event as MouseEvent).clientX),
  filter(x => x > 100),
  throttleTime(200)
);

// 3. Subscribe verwerking (uitvoeren van neveneffecten)
const subscription = processed$.subscribe({
  next: x => console.log('Click positie:', x),
  error: err => console.error(err),
  complete: () => console.log('voltooid')
});

Voordelen ​

  • Makkelijk debuggen - kan console.log en tap invoegen bij elke stap
  • Testbaar - processed$ kan onafhankelijk getest worden
  • Herbruikbaarheid - gebruik clicks$ en processed$ elders
  • Verbeterde leesbaarheid - code intentie is duidelijk

De fasescheidingssyntaxis is een van de meest praktische manieren om de problemen met RxJS op te lossen.

Zie Hoofdstuk 10: One-Liner Hell en Fasescheidingssyntaxis voor meer informatie.

Experimenteren en begrijpen (Starter Kit gebruiken) ​

Experiment 1: Verschil tussen Lazy en Eager ​

typescript
import { Observable } from 'rxjs';

console.log('=== Promise (Eager) ===');
const promise = new Promise((resolve) => {
  console.log('Promise uitgevoerd!');
  resolve(42);
});
console.log('Promise aanmaak voltooid');
promise.then(value => console.log('Promise resultaat:', value));

console.log('\n=== Observable (Lazy) ===');
const observable$ = new Observable(subscriber => {
  console.log('Observable uitgevoerd!');
  subscriber.next(42);
  subscriber.complete();
});
console.log('Observable creatie voltooid');
observable$.subscribe(value => console.log('Observable resultaat:', value));

// Uitvoer:
// === Promise (Eager) ===
// Promise uitgevoerd!
// Promise aanmaak voltooid
// Promise resultaat: 42
//
// === Observable (Lazy) ===
// Observable creatie voltooid
// Observable uitgevoerd!
// Observable resultaat: 42

Experiment 2: Verschil tussen Cold en Hot ​

typescript
import { interval } from 'rxjs';
import { take, share } from 'rxjs';

// Cold: onafhankelijk per abonnement
const cold$ = interval(1000).pipe(take(3));

console.log('Cold Observable:');
cold$.subscribe(x => console.log('Cold Subscribe 1:', x));
setTimeout(() => {
  cold$.subscribe(x => console.log('Cold Subscribe 2:', x));
}, 2000);

// Hot: gedeeld
const hot$ = interval(1000).pipe(take(3), share());

setTimeout(() => {
  console.log('\nHot Observable:');
  hot$.subscribe(x => console.log('Hot Subscribe 1:', x));
  setTimeout(() => {
    hot$.subscribe(x => console.log('Hot Subscribe 2:', x));
  }, 2000);
}, 6000);

Voer het uit in de leeromgeving om het verschil te ervaren.

Experiment 3: Declaratief vs. Imperatief ​

typescript
import { of } from 'rxjs';
import { map, filter } from 'rxjs';

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Imperatief
console.log('=== Imperatief ===');
const result1: number[] = [];
for (const num of numbers) {
  const doubled = num * 2;
  if (doubled > 10) {
    result1.push(doubled);
  }
}
console.log(result1);

// Declaratief
console.log('\n=== Declaratief ===');
of(...numbers).pipe(
  map(num => num * 2),
  filter(num => num > 10)
).subscribe(num => console.log(num));

Begripscontrole ​

Kijk of je de volgende vragen kunt beantwoorden.

markdown
## Basisconcepten
- [ ] Noem drie verschillen tussen Promise en Observable
- [ ] Leg het verschil uit tussen Lazy en Eager
- [ ] Leg het verschil uit tussen Cold en Hot met voorbeelden

## Praktijk
- [ ] Leg uit waarom verwerking niet voltooid mag worden binnen een subscribe
- [ ] Begrijp hoe geneste subscribe aan te passen
- [ ] Weet hoe je Cold Observable omzet in Hot

## Debuggen
- [ ] Identificeer de oorzaak wanneer een Observable niet wordt uitgevoerd
- [ ] Begrijp wat de oorzaak is dat een abonnement meerdere keren wordt uitgevoerd

Volgende stappen ​

Als je eenmaal een conceptueel begrip hebt, gaan we verder met de praktische uitdagingen.

  • Lifecycle Management - wanneer subscribe/unsubscribe
  • Operator Selectie - Criteria om te kiezen uit 100+ operatoren

Gerelateerde secties ​

Uitgebracht onder de CC-BY-4.0 licentie.