Skip to content

Barrières voor Lifecycle Management

Een van de grootste valkuilen van RxJS is Lifecycle Management. Fouten in "wanneer te subscriben" en "wanneer te unsubscriben" kunnen leiden tot geheugenlekken en bugs.

Wanneer te Subscriben

Basisprincipe: Subscribe Pas op het Laatste Moment

❌ Slecht Voorbeeld: Subscribe in het Midden

typescript
import { interval } from 'rxjs';

function getEvenNumbers() {
  const numbers$ = interval(1000);

  // subscribe hier
  numbers$.subscribe(n => {
    if (n % 2 === 0) {
      console.log(n); // hoe geven we dit extern door?
    }
  });
}

✅ Goed Voorbeeld: Return Observable, Subscribe bij Aanroeper

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

function getEvenNumbers() {
  return interval(1000).pipe(
    filter(n => n % 2 === 0),
    take(5)
  );
}

// subscribe aan de gebruikerskant
const subscription = getEvenNumbers().subscribe(n => {
  console.log(n);
});

💡 Uitleg

  • Slecht voorbeeld: subscribe in een functie verliest controle (kan niet annuleren, kan niet samenstellen)
  • Goed voorbeeld: return Observable, aanroeper kan controleren

Subscribe Triggert "Bijwerkingen"

❌ Slecht Voorbeeld: Meerdere Bijwerkingen in Subscribe

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

const button = document.querySelector('button')!;

fromEvent(button, 'click')
  .pipe(map(() => Math.random()))
  .subscribe(randomValue => {
    // Bijwerking 1: DOM-manipulatie
    document.querySelector('#result')!.textContent = randomValue.toString();

    // Bijwerking 2: API-aanroep
    fetch('/api/log', {
      method: 'POST',
      body: JSON.stringify({ value: randomValue })
    });

    // Bijwerking 3: Lokale opslag
    localStorage.setItem('lastValue', randomValue.toString());
  });

✅ Goed Voorbeeld: Isoleer Bijwerkingen en Subscribe Alleen naar Wat U Nodig Hebt

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

const button = document.querySelector('button')!;

const randomClicks$ = fromEvent(button, 'click').pipe(
  map(() => Math.random())
);

// als alleen DOM-updates nodig zijn
randomClicks$.subscribe(value => {
  document.querySelector('#result')!.textContent = value.toString();
});

// Als alleen logging vereist is
randomClicks$.subscribe(value => {
  fetch('/api/log', {
    method: 'POST',
    body: JSON.stringify({ value })
  });
});

💡 Uitleg

  • subscribe = uitvoeringspunt van bijwerking
  • Als bijwerkingen onafhankelijk zijn: splits in meerdere subscribe (kunnen individueel worden gecontroleerd)
  • Als bijwerkingen altijd als set worden uitgevoerd: OK om ze samen te groeperen in één subscribe
  • Als bijwerkingen nodig zijn in de pipeline: gebruik tap operator

Wanneer te Subscriben: Beslissingsstroomdiagram

De Volledige Lifecycle van een Subscription

Het volgende toestandsovergangsdiagram toont de toestanden die een Observable subscription doorloopt en waarin het eindigt.

Belangrijke Punten voor Lifecycle Management

  • Subscribed: Toestand met risico op geheugenlek
  • complete/error: Automatische cleanup (geen unsubscribe vereist)
  • unsubscribe: Handmatige cleanup vereist (vooral voor oneindige streams)

Wanneer Moet Ik Unsubscriben?

Basisprincipe: Altijd Unsubscriben Nadat U Hebt Gesubscribed

❌ Slecht Voorbeeld: Niet Unsubscriben → Geheugenlek

typescript
import { interval } from 'rxjs';

const button = document.querySelector('button')!;

function startTimer() {
  interval(1000).subscribe(n => {
    console.log(n);
  });
  // Deze subscription duurt voor altijd!
}

// bij elke knopclik wordt een nieuwe subscription toegevoegd
button.addEventListener('click', startTimer);
// 10 kliks = 10 subscriptions die tegelijkertijd draaien!

✅ Goed Voorbeeld: Unsubscribe met unsubscribe

typescript
import { interval } from 'rxjs';

function startTimer() {
  const subscription = interval(1000).subscribe(n => {
    console.log(n);
  });

  // Unsubscribe na 5 seconden
  setTimeout(() => {
    subscription.unsubscribe();
    console.log('Unsubscribed');
  }, 5000);
}

💡 Uitleg

  • Oneindige streams (interval, fromEvent, enz.) moeten unsubscribe
  • Als niet unsubscribe, gaan geheugenlekkage + onnodige verwerking door

Gevallen Waarin Unsubscribe Niet Nodig Is

✅ Observable die Automatisch Complete

typescript
of(1, 2, 3).subscribe(n => console.log(n));
// automatisch opgeruimd na complete

from([1, 2, 3]).subscribe(n => console.log(n));
// automatisch opgeruimd na complete

✅ Voltooiing Gegarandeerd door take, enz.

typescript
interval(1000).pipe(
  take(5) // automatisch complete na 5 keer
).subscribe(n => console.log(n));

✅ Afsluiten met Error

typescript
throwError(() => new Error('Error')).subscribe({
  error: err => console.error(err)
});

✅ EMPTY (Onmiddellijk Complete)

typescript
EMPTY.subscribe(() => console.log('not executed'));

💡 Uitleg

unsubscribe is niet vereist als:

  1. Observable aangeroepen door complete() - automatisch opgeruimd
  2. error() wordt aangeroepen - ook automatisch opgeruimd
  3. Gegarandeerde voltooiing door take(n), enz. - expliciet complete

Belangrijk

Oneindige streams (interval, fromEvent, Subject, enz.) moeten unsubscribe!

Stroomdiagram voor het Bepalen of Unsubscribe Vereist Is

Bij twijfel is het veilig om te unsubscriben!

Patronen om Geheugenlekken te Voorkomen

Patroon 1: Beheerd door Subscription Object

typescript
import { interval, fromEvent } from 'rxjs';
import { Subscription } from 'rxjs';

class MyComponent {
  private subscription = new Subscription();

  ngOnInit() {
    // Voeg meerdere subscriptions toe aan één Subscription
    this.subscription.add(
      interval(1000).subscribe(n => console.log('Timer:', n))
    );

    this.subscription.add(
      fromEvent(document, 'click').subscribe(() => console.log('Click!'))
    );

    this.subscription.add(
      fromEvent(window, 'resize').subscribe(() => console.log('Resize!'))
    );
  }

  ngOnDestroy() {
    // Unsubscribe alle subscriptions in één keer
    this.subscription.unsubscribe();
  }
}

💡 Voordelen

  • Beheer meerdere subscriptions met één object
  • Bulkannulering met ngOnDestroy
  • Gemakkelijk toe te voegen en te verwijderen

Patroon 2: Beheer via Array

typescript
import { interval, fromEvent } from 'rxjs';
import { Subscription } from 'rxjs';

class MyComponent {
  private subscriptions: Subscription[] = [];

  ngOnInit() {
    this.subscriptions.push(
      interval(1000).subscribe(n => console.log('Timer:', n))
    );

    this.subscriptions.push(
      fromEvent(document, 'click').subscribe(() => console.log('Click!'))
    );
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions = [];
  }
}

💡 Voordelen

  • Flexibel beheer met array-bewerkingen
  • Kan ook individueel worden gedeactiveerd
  • Gemakkelijk te debuggen (controleer array in console.log)

Patroon 3: takeUntil Patroon (Aanbevolen)

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

class MyComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    // voeg takeUntil(this.destroy$) toe aan alle subscriptions
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => console.log('Timer:', n));

    fromEvent(document, 'click').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => console.log('Click!'));

    fromEvent(window, 'resize').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => console.log('Resize!'));
  }

  ngOnDestroy() {
    // Unsubscribe alles in één next()
    this.destroy$.next();
    this.destroy$.complete();
  }
}

💡 Voordelen

  • Meest declaratief - pipeline maakt voltooiingsvoorwaarden expliciet
  • Geen Subscription object vereist - geheugenefficiënt
  • Gemakkelijkst te lezen - code zegt "complete met destroy$"

Volledige Gids voor het takeUntil Patroon

Basispatroon

typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

// deze subscription gaat door totdat destroy$ next() doet
interval(1000).pipe(
  takeUntil(destroy$)
).subscribe(n => console.log(n));

// Stop alle subscriptions na 5 seconden
setTimeout(() => {
  destroy$.next();
  destroy$.complete();
}, 5000);

Marble Diagram

interval(1000):  --0--1--2--3--4--5--6--7-->
destroy$:        ----------X

                       next() aanroep

takeUntil resultaat:   --0--1--2|

                       complete

Toepassen op Meerdere Observables

typescript
import { interval, fromEvent, timer, Subject } from 'rxjs';
import { takeUntil, map } from 'rxjs';

const destroy$ = new Subject<void>();

// Patroon: gebruik dezelfde destroy$ voor alle streams
interval(1000).pipe(
  takeUntil(destroy$),
  map(n => `Timer: ${n}`)
).subscribe(console.log);

fromEvent(document, 'click').pipe(
  takeUntil(destroy$),
  map(() => 'Click!')
).subscribe(console.log);

timer(2000).pipe(
  takeUntil(destroy$),
  map(() => 'Timer voltooid')
).subscribe(console.log);

// batch stop
function cleanup() {
  destroy$.next();
  destroy$.complete();
}

// Voorbeeld: roep cleanup() aan bij paginaovergang
window.addEventListener('beforeunload', cleanup);

Veelgemaakte Fouten in het takeUntil Patroon

Fout 1: takeUntil Staat op de Verkeerde Positie

❌ Slecht Voorbeeld: map Vóór takeUntil

typescript
import { interval, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

interval(1000).pipe(
  takeUntil(destroy$),  // als complete hier...
  map(n => n * 2)       // map kan worden uitgevoerd
).subscribe(console.log);

✅ Goed Voorbeeld: takeUntil Als Laatste Geplaatst

typescript
import { interval, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

interval(1000).pipe(
  map(n => n * 2),
  takeUntil(destroy$)  // na alle operators
).subscribe(console.log);

💡 Uitleg

  • takeUntil wordt als laatste geplaatst indien mogelijk
  • Uitzondering: soms geplaatst vóór multicast operators zoals shareReplay

Fout 2: Destroy$ Niet Completen

❌ Slecht Voorbeeld: complete() Niet Aanroepen

typescript
import { Subject } from 'rxjs';

const destroy$ = new Subject<void>();

function cleanup() {
  destroy$.next();
  // ❌ roep complete() niet aan
}

// Probleem: destroy$ zelf veroorzaakt een geheugenlek

✅ Goed Voorbeeld: Roep Zowel next() als complete() Aan

typescript
import { Subject } from 'rxjs';

const destroy$ = new Subject<void>();

function cleanup() {
  destroy$.next();
  destroy$.complete();
}

💡 Uitleg

  • Als alleen next() wordt gebruikt, blijft destroy$ zelf gesubscribed
  • Roep altijd ook complete() aan

Fout 3: Proberen te Hergebruiken

❌ Slecht Voorbeeld: Een Voltooid Subject Hergebruiken

typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

function start() {
  interval(1000).pipe(
    takeUntil(destroy$)
  ).subscribe(console.log);
}

function stop() {
  destroy$.next();
  destroy$.complete();
}

start();
setTimeout(stop, 3000);

// ❌ Probleem: destroy$ is al complete, dus start() opnieuw zal onmiddellijk eindigen
setTimeout(start, 5000); // dit werkt niet

✅ Goed Voorbeeld: Regenereer destroy$

typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

class MyComponent {
  private destroy$ = new Subject<void>();

  start() {
    // regenereer als al voltooid
    if (this.destroy$.closed) {
      this.destroy$ = new Subject<void>();
    }

    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(console.log);
  }

  stop() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

💡 Uitleg

  • Subject kan niet worden hergebruikt zodra complete
  • Als het opnieuw moet worden geopend, maak een nieuw Subject aan

Best Practices voor Subscription Management

Best Practice 1: Heb destroy$ Per Component/Class

typescript
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

class UserProfileComponent {
  private destroy$ = new Subject<void>();

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUser().pipe(
      takeUntil(this.destroy$)
    ).subscribe(user => {
      console.log(user);
    });

    this.userService.getUserPosts().pipe(
      takeUntil(this.destroy$)
    ).subscribe(posts => {
      console.log(posts);
    });
  }

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

💡 Voordelen

  • Consistentie - zelfde patroon in alle componenten
  • Onderhoudbaarheid - geen ngOnDestroy-wijzigingen bij toevoegen van nieuwe subscriptions
  • Veiligheid - geen unsubscribe-lekken

Best Practice 2: Maak Gebruik van AsyncPipe (voor Angular)

typescript
import { Component } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-user-profile',
  template: `
    <!-- AsyncPipe subscribe/unsubscribe automatisch -->
    <div *ngIf="user$ | async as user">
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    </div>

    <ul>
      <li *ngFor="let post of posts$ | async">
        {{ post.title }}
      </li>
    </ul>
  `
})
export class UserProfileComponent {
  user$: Observable<User>;
  posts$: Observable<Post[]>;

  constructor(private userService: UserService) {
    // geef de Observable direct door aan de template
    this.user$ = this.userService.getUser();
    this.posts$ = this.userService.getUserPosts();

    // Geen ngOnDestroy vereist! AsyncPipe zal het automatisch vrijgeven
  }
}

💡 Voordelen

  • Automatisch unsubscriben - unsubscribet automatisch wanneer component wordt vernietigd
  • OnPush-ondersteuning - geoptimaliseerde change detection
  • Code-eenvoud - geen boilerplate subscribe/unsubscribe

Best Practice 3: Verander Strategie op Basis van Lange vs. Korte Levensduur

typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, interval, fromEvent } from 'rxjs';
import { takeUntil } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class DataService {
  // ✅ Toestand gedeeld over services (langlevend)
  // → Blijf gesubscribed tot de applicatie eindigt
  private userState$ = new BehaviorSubject<User | null>(null);

  getUser() {
    return this.userState$.asObservable();
  }

  // ❌ subscribe niet direct in de component
  // ✅ subscribe via AsyncPipe of takeUntil
}

class MyComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    // ✅ Subscription gekoppeld aan component lifecycle (kortlevend)
    // → altijd uitschakelen door ngOnDestroy
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => console.log(n));

    fromEvent(window, 'resize').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => console.log('Resize'));
  }

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

💡 Principe

Subscription TypeLifecycleBeheermethode
Globale toestandApplicatiebreedBehaviorSubject + AsyncPipe
Pagina/Route SpecifiekTerwijl route actief istakeUntil(routeDestroy$)
Component specifiekTerwijl component bestaattakeUntil(destroy$) of AsyncPipe
Eenmalige API-aanroepTot voltooiingtake(1) of first()

Best Practice 4: Stel Expliciete Voltooiingsvoorwaarden In

❌ Slecht Voorbeeld: Onbekend Wanneer Het Zal Eindigen

typescript
import { fromEvent } from 'rxjs';

fromEvent(document, 'click').subscribe(() => {
  console.log('Click');
});

✅ Goed Voorbeeld 1: Frequentielimiet

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

fromEvent(document, 'click').pipe(
  take(5) // eindigt automatisch na 5 keer
).subscribe(() => {
  console.log('Click (max 5 keer)');
});

✅ Goed Voorbeeld 2: Tijdslimiet

typescript
import { fromEvent, timer } from 'rxjs';
import { takeUntil } from 'rxjs';

const timeout$ = timer(10000); // na 10 seconden
fromEvent(document, 'click').pipe(
  takeUntil(timeout$)
).subscribe(() => {
  console.log('Click (binnen 10 seconden)');
});

✅ Goed Voorbeeld 3: Meerdere Uitgangsvoorwaarden

typescript
import { fromEvent, Subject, merge } from 'rxjs';
import { takeUntil, take } from 'rxjs';

const destroy$ = new Subject<void>();
const maxClicks$ = fromEvent(document, 'click').pipe(take(10));

fromEvent(document, 'mousemove').pipe(
  takeUntil(merge(destroy$, maxClicks$)) // eindig met een van beide
).subscribe(() => {
  console.log('Mouse move');
});

💡 Principes

  • Vermeld expliciet "wanneer het zal eindigen" - Vermijd oneindige streams
  • Stel voltooiingsvoorwaarden in met take, first, takeWhile, takeUntil, enz.
  • Koppel het aan een lifecycle (destroy$, timeout$, enz.)

Begripscontrolelijst

Controleer of u de volgende vragen kunt beantwoorden.

markdown
## Basiskennis
- [ ] Leg uit wat er gebeurt wanneer u subscribet naar een Observable
- [ ] Onderscheid tussen gevallen waarin unsubscribe noodzakelijk is en gevallen waarin het niet nodig is
- [ ] Leg de oorzaken van geheugenlekken uit

## Patronen Toepassen
- [ ] Beheer meerdere subscriptions met het Subscription object
- [ ] Implementeer het takeUntil patroon
- [ ] Plaats destroy$ correct (laatste operator)

## Best Practices
- [ ] Weet wanneer AsyncPipe te gebruiken
- [ ] Kan onderscheid maken tussen en beheren van langlevende en kortlevende subscriptions
- [ ] Kan expliciete voltooiingsvoorwaarden instellen

## Debugging
- [ ] Weet hoe geheugenlekken te detecteren
- [ ] Vind unsubscribe-lekken
- [ ] Kan subscription count controleren in Browser DevTools

Volgende Stappen

Zodra u lifecycle management begrijpt, is de volgende stap het leren over Operator Selectie.

Moeilijkheden bij Operator Selectie - Criteria voor het kiezen van de juiste uit meer dan 100 operators

Gerelateerde Pagina's

🎯 Oefeningen

Oefening 1: Repareer een Geheugenlek

De volgende code heeft een geheugenlek. Repareer het alstublieft.

typescript
class ChatComponent {
  ngOnInit() {
    interval(5000).subscribe(() => {
      this.chatService.checkNewMessages().subscribe(messages => {
        console.log('Nieuwe berichten:', messages);
      });
    });
  }
}
Voorbeeldoplossing
typescript
class ChatComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(5000).pipe(
      takeUntil(this.destroy$),
      switchMap(() => this.chatService.checkNewMessages())
    ).subscribe(messages => {
      console.log('Nieuwe berichten:', messages);
    });
  }

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

Reparaties

  1. Voeg destroy$ Subject toe
  2. Stop interval met takeUntil(this.destroy$)
  3. Los geneste subscribe op met switchMap
  4. Cleanup met ngOnDestroy

Oefening 2: Het Juiste Patroon Kiezen

Kies in het volgende scenario het beste subscription management patroon.

  1. HTTP-verzoek (eenmalig)
  2. WebSocket-verbinding (tijdens het bestaan van de component)
  3. Globale gebruikerstoestand (applicatiebreed)
Voorbeeldoplossing

1. HTTP-verzoek (eenmalig)

typescript
// ✅ take(1) of first() - autocomplete eenmaal
this.http.get('/api/user').pipe(
  take(1)
).subscribe(user => console.log(user));

// of AsyncPipe (voor Angular)
user$ = this.http.get('/api/user');

2. WebSocket-verbinding (in aanwezigheid van een component)

typescript
// ✅ takeUntil patroon - verbreek verbinding wanneer component wordt vernietigd
private destroy$ = new Subject<void>();

ngOnInit() {
  this.websocket.connect().pipe(
    takeUntil(this.destroy$)
  ).subscribe(message => console.log(message));
}

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

3. Globale gebruikerstoestand (applicatiebreed)

typescript
// ✅ BehaviorSubject + AsyncPipe - geen unsubscribe vereist
@Injectable({ providedIn: 'root' })
class AuthService {
  private userState$ = new BehaviorSubject<User | null>(null);

  getUser() {
    return this.userState$.asObservable();
  }
}

// gebruikt in component
user$ = this.authService.getUser(); // Subscribe met AsyncPipe

Uitgebracht onder de CC-BY-4.0 licentie.