Skip to content

Moeilijkheden in Statusbeheer

In RxJS zijn "Ik wil status delen met meerdere componenten" en "Ik wil API-resultaten cachen" zeer gebruikelijke vereisten, maar het kiezen van de juiste methode is moeilijk. Deze pagina beschrijft praktische patronen voor statusbeheer en stream-deling.

Subject vs BehaviorSubject vs ReplaySubject

Typen en Kenmerken van Subject

SubjectInitiële waardeGedrag bij abonnementVeelvoorkomende use cases
SubjectGeenOntvang alleen de waarde na abonnementEvent Bus, Notificatiesysteem
BehaviorSubjectVereistOntvang laatste waarde onmiddellijkHuidige status (ingelogd, geselecteerd item)
ReplaySubjectGeenOntvang laatste N waardenGeschiedenis, log, bewerkingsrecord
AsyncSubjectGeenOntvang alleen de laatste waarde bij voltooiingEnkel asynchroon resultaat (wordt niet veel gebruikt)

Visualiseer het Verschil Tussen Subject-type en Werking

Het volgende diagram toont welke waarde elk Subject ontvangt bij abonnement.

Selectiecriteria

  • Subject: Eventnotificatie (verleden niet nodig)
  • BehaviorSubject: Statusbeheer (huidige waarde nodig)
  • ReplaySubject: Geschiedenisbeheer (verleden N vereist)

Praktisch Voorbeeld 1: Subject (Event Bus)

❌ Slecht voorbeeld: geen waarden worden geaccepteerd vóór abonnement

typescript
import { Subject } from 'rxjs';

const notifications$ = new Subject<string>();

notifications$.next('Notificatie 1'); // niemand is nog geabonneerd

notifications$.subscribe(msg => {
  console.log('Ontvangen:', msg);
});

notifications$.next('Notificatie 2');
notifications$.next('Notificatie 3');

// Output:
// Ontvangen: Notificatie 2
// Ontvangen: Notificatie 3
// ('Notificatie 1' wordt niet ontvangen)

✅ Goed voorbeeld: gebruik als event bus (alleen events na abonnement verwerken)

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

class EventBus {
  private events$ = new Subject<{ type: string; payload: any }>();

  emit(type: string, payload: any) {
    this.events$.next({ type, payload });
  }

  on(type: string) {
    return this.events$.pipe(
      filter(event => event.type === type),
      map(event => event.payload)
    );
  }
}

const bus = new EventBus();

// start abonnement
bus.on('userLogin').subscribe(user => {
  console.log('login:', user);
});

// event uitgegeven
bus.emit('userLogin', { id: 1, name: 'Alice' }); // ✅ Ontvangen
// login: {id: 1, name: 'Alice'}

Subject Gebruik

  • Event-driven architectuur: Losjes gekoppelde communicatie tussen componenten
  • Notificatiesysteem: Real-time notificatielevering
  • Wanneer waarden uit het verleden niet nodig zijn: Alleen post-abonnement events moeten worden verwerkt

Praktisch Voorbeeld 2: BehaviorSubject (Statusbeheer)

❌ Slecht voorbeeld: Subject kent huidige status niet

typescript
import { Subject } from 'rxjs';

const isLoggedIn$ = new Subject<boolean>();

// gebruiker is ingelogd
isLoggedIn$.next(true);

// Component abonneert zich daarna
isLoggedIn$.subscribe(status => {
  console.log('login status:', status); // niets wordt uitgevoerd
});

✅ Goed voorbeeld: krijg huidige status onmiddellijk met BehaviorSubject

typescript
import { BehaviorSubject } from 'rxjs';

class AuthService {
  private isLoggedIn$ = new BehaviorSubject<boolean>(false); // initiële waarde: false

  login(username: string, password: string) {
    // login proces...
    this.isLoggedIn$.next(true);
  }

  logout() {
    this.isLoggedIn$.next(false);
  }

  // extern publiceren als alleen-lezen
  get isLoggedIn() {
    return this.isLoggedIn$.asObservable();
  }

  // Krijg huidige waarde synchroon (alleen gebruikt voor speciale gevallen)
  get currentStatus(): boolean {
    return this.isLoggedIn$.value;
  }
}

const auth = new AuthService();

auth.login('user', 'pass');

// Abonneer later en krijg onmiddellijk de huidige status (true)
auth.isLoggedIn.subscribe(status => {
  console.log('login status:', status); // login status: true
});

Waar BehaviorSubject te Gebruiken

  • Behoud huidige status: Loginstatus, geselecteerde items, configuratiewaarden
  • Vereist onmiddellijke waarde bij abonnement: Huidige status is vereist voor initiële weergave in UI
  • Monitor statuswijzigingen: Reactief updaten wanneer status verandert

Praktisch Voorbeeld 3: ReplaySubject (Geschiedenisbeheer)

✅ Goed voorbeeld: Herhaal de afgelopen N waarden

typescript
import { ReplaySubject } from 'rxjs';

class SearchHistoryService {
  // behoud geschiedenis van laatste 5 zoekopdrachten
  private history$ = new ReplaySubject<string>(5);

  addSearch(query: string) {
    this.history$.next(query);
  }

  getHistory() {
    return this.history$.asObservable();
  }
}

const searchHistory = new SearchHistoryService();

// Voer zoekopdracht uit
searchHistory.addSearch('TypeScript');
searchHistory.addSearch('RxJS');
searchHistory.addSearch('Angular');

// De laatste 3 resultaten kunnen direct worden opgehaald, zelfs als je later abonneert
searchHistory.getHistory().subscribe(query => {
  console.log('Zoekgeschiedenis:', query);
});

// Output:
// Zoekgeschiedenis: TypeScript
// Zoekgeschiedenis: RxJS
// Zoekgeschiedenis: Angular

ReplaySubject Gebruik

  • Actiegeschiedenis: Zoekgeschiedenis, bewerkingsgeschiedenis, navigatiegeschiedenis
  • Log/Audit Trail: Record van eerdere bewerkingen
  • Late Abonnementondersteuning: Als je waarden uit het verleden wilt ontvangen zelfs als je abonnement laat start

Onderscheid Tussen share en shareReplay

Probleem: Dubbele Uitvoering van Cold Observable

❌ Slecht voorbeeld: API wordt meerdere keren aangeroepen met meerdere subscribe

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

const users$ = ajax.getJSON('/api/users');

// abonnement1
users$.subscribe(users => {
  console.log('Component A:', users);
});

// Abonnement 2
users$.subscribe(users => {
  console.log('Component B:', users);
});

// Probleem: API wordt twee keer aangeroepen
// GET /api/users (1e keer)
// GET /api/users (2e keer)

✅ Goed voorbeeld: converteer naar Hot met share (deel uitvoering)

typescript
import { ajax } from 'rxjs/ajax';
import { share } from 'rxjs';

const users$ = ajax.getJSON('/api/users').pipe(
  share() // deel uitvoering
);

// abonnement1
users$.subscribe(users => {
  console.log('Component A:', users);
});

// Abonnement 2 (als je onmiddellijk abonneert)
users$.subscribe(users => {
  console.log('Component B:', users);
});

// ✅ API wordt slechts één keer aangeroepen
// GET /api/users (slechts één keer)

share Valkuilen

share() reset de stream** wanneer het **laatste abonnement wordt beëindigd. Het zal opnieuw worden uitgevoerd de volgende keer dat je abonneert.

typescript
const data$ = fetchData().pipe(share());

// abonnement1
const sub1 = data$.subscribe();

// abonnement2
const sub2 = data$.subscribe();

sub1.unsubscribe();
sub2.unsubscribe(); // iedereen beëindigt abonnement → reset

// opnieuw abonneren → fetchData() wordt opnieuw uitgevoerd
data$.subscribe();

shareReplay: Cache en Hergebruik Resultaten

✅ Goed voorbeeld: cache met shareReplay

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

const users$ = ajax.getJSON('/api/users').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // bufferSize: 1 → cache de laatste enkele waarde
  // refCount: true → cache wordt gewist wanneer alle abonnementen worden beëindigd
);

// abonnement1
users$.subscribe(users => {
  console.log('Component A:', users);
});

// Abonneer 1 seconde later Abonnement 2 (vertraagd abonnement, maar nog steeds opgehaald uit cache)
setTimeout(() => {
  users$.subscribe(users => {
    console.log('Component B:', users); // onmiddellijk ophalen uit cache
  });
}, 1000);

// ✅ API wordt slechts één keer aangeroepen, en het resultaat wordt gecachet

share vs shareReplay Vergelijking

Functieshare()shareReplay(1)
Nieuw abonnement tijdens abonnementDeel dezelfde streamDeel dezelfde stream
Abonneer laatOntvang alleen nieuwe waardenOntvang laatste gecachte waarden
Na alle abonnementen worden beëindigdReset streamBehoud cache (als refCount: false)
GeheugenNiet behoudenBehoud cache
Use caseReal-time gegevensdelingCache API-resultaten

✅ Goed voorbeeld: juiste configuratie van shareReplay

typescript
import { shareReplay } from 'rxjs';

// Patroon 1: persistente cache (niet aanbevolen)
const data1$ = fetchData().pipe(
  shareReplay({ bufferSize: 1, refCount: false })
  // refCount: false → geheugenlek waarschuwing
);

// Patroon 2: cache met automatische opruiming (aanbevolen)
const data2$ = fetchData().pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // refCount: true → Beëindig alle abonnementen en wis cache
);

// Patroon 3: Cache met TTL (RxJS 7.4+)
const data3$ = fetchData().pipe(
  shareReplay({
    bufferSize: 1,
    refCount: true,
    windowTime: 5000 // gooi cache weg na 5 seconden
  })
);

Geheugenlek Waarschuwing

shareReplay({ refCount: false }) kan geheugenlekken veroorzaken omdat de cache persistent blijft. Gebruik in principe refCount: true.

Praktisch Gebruik van Hot vs Cold

Cold Kenmerken: Uitvoeren Per Abonnement

typescript
import { Observable } from 'rxjs';

const cold$ = new Observable<number>(subscriber => {
  console.log('🔵 Uitvoering gestart');
  subscriber.next(Math.random());
  subscriber.complete();
});

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

// Output:
// 🔵 Start Uitvoering
// Abonnement 1: 0.123
// 🔵 Start Uitvoering
// Abonnement 2: 0.456
// (twee uitvoeringen, verschillende waarden)

Hot Kenmerken: Gedeelde Uitvoering

typescript
import { Subject } from 'rxjs';

const hot$ = new Subject<number>();

hot$.subscribe(v => console.log('Abonnement 1:', v));
hot$.subscribe(v => console.log('Abonnement 2:', v));

hot$.next(Math.random());

// Output:
// abonnement1: 0.789
// Abonnement 2: 0.789
// (delen dezelfde waarde)

Gebruikscriteria

VereistenColdHot
Onafhankelijke uitvoering nodig
Wil uitvoering delen
Verschillende waarden voor verschillende abonnees
Real-time gegevensdistributie
Delen van API-aanroepen❌ (converteer met share)

✅ Goed voorbeeld: juiste conversie

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

// Cold: elke abonnee is een onafhankelijke timer
const coldTimer$ = interval(1000);

// Cold → Hot: deel timer
const hotTimer$ = interval(1000).pipe(share());

// Cold: klik event (onafhankelijke listener registratie voor elk abonnement)
const clicks$ = fromEvent(document, 'click');

// Cold → Hot: cache API-resultaten
const cachedData$ = ajax.getJSON('/api/data').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);

Gecentraliseerd Statusbeheerpatroon

Patroon 1: Statusbeheer in de Service Class

typescript
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs';

interface User {
  id: number;
  name: string;
  email: string;
}

class UserStore {
  // privé BehaviorSubject
  private users$ = new BehaviorSubject<User[]>([]);

  // alleen-lezen Observable voor openbaar gebruik
  get users(): Observable<User[]> {
    return this.users$.asObservable();
  }

  // krijg een specifieke gebruiker
  getUser(id: number): Observable<User | undefined> {
    return this.users.pipe(
      map(users => users.find(u => u.id === id))
    );
  }

  // update status
  addUser(user: User) {
    const currentUsers = this.users$.value;
    this.users$.next([...currentUsers, user]);
  }

  updateUser(id: number, updates: Partial<User>) {
    const currentUsers = this.users$.value;
    const updatedUsers = currentUsers.map(u =>
      u.id === id ? { ...u, ...updates } : u
    );
    this.users$.next(updatedUsers);
  }

  removeUser(id: number) {
    const currentUsers = this.users$.value;
    this.users$.next(currentUsers.filter(u => u.id !== id));
  }
}

// Gebruik
const store = new UserStore();

// Abonneer
store.users.subscribe(users => {
  console.log('Gebruikerslijst:', users);
});

// update status
store.addUser({ id: 1, name: 'Alice', email: 'alice@example.com' });
store.updateUser(1, { name: 'Alice Smith' });

Patroon 2: Statusbeheer met Scan

typescript
import { Subject } from 'rxjs';
import { scan, startWith } from 'rxjs';

interface State {
  count: number;
  items: string[];
}

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'ADD_ITEM'; payload: string }
  | { type: 'RESET' };

const actions$ = new Subject<Action>();

const initialState: State = {
  count: 0,
  items: []
};

const state$ = actions$.pipe(
  scan((state, action) => {
    switch (action.type) {
      case 'INCREMENT':
        return { ...state, count: state.count + 1 };
      case 'DECREMENT':
        return { ...state, count: state.count - 1 };
      case 'ADD_ITEM':
        return { ...state, items: [...state.items, action.payload] };
      case 'RESET':
        return initialState;
      default:
        return state;
    }
  }, initialState),
  startWith(initialState)
);

// Abonneer
state$.subscribe(state => {
  console.log('Huidige status:', state);
});

// geef actie uit
actions$.next({ type: 'INCREMENT' });
actions$.next({ type: 'ADD_ITEM', payload: 'appel' });
actions$.next({ type: 'INCREMENT' });

// Output:
// huidige status: { count: 0, items: [] }
// huidige status: { count: 1, items: [] }
// huidige status: { count: 1, items: ['appel'] }
// huidige status: { count: 2, items: ['appel'] }

Veelvoorkomende Valkuilen

Valkuil 1: Externe Blootstelling van Subject

❌ Slecht voorbeeld: publiceren van de Subject direct

typescript
import { BehaviorSubject } from 'rxjs';

class BadService {
  // ❌ kan direct van buitenaf worden gewijzigd
  public state$ = new BehaviorSubject<number>(0);
}

const service = new BadService();

// kan van buitenaf zonder toestemming worden gewijzigd
service.state$.next(999); // ❌ encapsulatie is verbroken

✅ Goed voorbeeld: beschermd door asObservable()

typescript
import { BehaviorSubject } from 'rxjs';

class GoodService {
  private _state$ = new BehaviorSubject<number>(0);

  // publiceer als alleen-lezen
  get state() {
    return this._state$.asObservable();
  }

  // kan alleen worden gewijzigd door toegewijde methoden
  increment() {
    this._state$.next(this._state$.value + 1);
  }

  decrement() {
    this._state$.next(this._state$.value - 1);
  }
}

const service = new GoodService();

// ✅ alleen-lezen
service.state.subscribe(value => console.log(value));

// ✅ Wijzigingen worden gedaan via toegewijde methoden
service.increment();

// ❌ Kan niet direct worden gewijzigd (compile fout)
// service.state.next(999); // Fout: Property 'next' does not exist

Valkuil 2: shareReplay Geheugenlek

❌ Slecht voorbeeld: geheugenlek met refCount: false

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

const data$ = interval(1000).pipe(
  take(100),
  shareReplay({ bufferSize: 1, refCount: false })
  // ❌ refCount: false → cache blijft voor altijd
);

// de stream blijft intern actief zelfs na abonneren en beëindigen van abonnement
const sub = data$.subscribe();
sub.unsubscribe();

// cache blijft → geheugenlek

✅ Goed voorbeeld: automatische opruiming met refCount: true

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

const data$ = interval(1000).pipe(
  take(100),
  shareReplay({ bufferSize: 1, refCount: true })
  // ✅ refCount: true → automatische opruiming met alle beëindigingen van abonnementen
);

const sub1 = data$.subscribe();
const sub2 = data$.subscribe();

sub1.unsubscribe();
sub2.unsubscribe(); // beëindig alle abonnementen → stop stream, wis cache

Valkuil 3: Synchrone Waarde Ophalen

❌ Slecht voorbeeld: te afhankelijk van value

typescript
import { BehaviorSubject } from 'rxjs';

class CounterService {
  private count$ = new BehaviorSubject(0);

  increment() {
    // ❌ te veel vertrouwen op value
    const current = this.count$.value;
    this.count$.next(current + 1);
  }

  // ❌ stelt synchrone acquisitie bloot
  getCurrentCount(): number {
    return this.count$.value;
  }
}

✅ Goed voorbeeld: houd het reactief

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

class CounterService {
  private count$ = new BehaviorSubject(0);

  get count() {
    return this.count$.asObservable();
  }

  increment() {
    // ✅ Het is OK om value intern te gebruiken
    this.count$.next(this.count$.value + 1);
  }

  // ✅ Retourneer met Observable
  isPositive() {
    return this.count$.pipe(
      map(count => count > 0)
    );
  }
}

Begrip Checklist

Zorg ervoor dat je de volgende vragen kunt beantwoorden.

markdown
## Basisbegrip
- [ ] Leg het verschil uit tussen Subject, BehaviorSubject en ReplaySubject
- [ ] Begrijp waarom BehaviorSubject een initiële waarde nodig heeft
- [ ] Begrijp de betekenis van bufferSize in ReplaySubject

## Hot/Cold
- [ ] Leg het verschil uit tussen Cold Observable en Hot Observable
- [ ] Leg het verschil uit tussen share en shareReplay
- [ ] Begrijp de rol van shareReplay's refCount optie

## Statusbeheer
- [ ] Kan de Subject beschermen met asObservable() zonder het naar buiten bloot te stellen
- [ ] Implementeer statusbeheerpatronen met BehaviorSubject
- [ ] Begrijp statusbeheerpatronen met scan

## Geheugenbeheer
- [ ] Weet hoe je geheugenlekken in shareReplay voorkomt
- [ ] Leg het verschil uit tussen refCount: true en false
- [ ] Wis de cache op het juiste moment

Volgende Stappen

Zodra je statusbeheer en delen begrijpt, is de volgende stap om te leren hoe je meerdere streams combineert.

Meerdere stream combinatie - Hoe combineLatest, zip, withLatestFrom te gebruiken

Gerelateerde Pagina's

🎯 Oefenproblemen

Probleem 1: Kiezen van een Geschikte Subject

Kies de beste Subject voor het volgende scenario.

  1. Beheer gebruikersloginstatus (standaard: uitgelogd)
  2. Levering van notificatieberichten (alleen berichten weergeven na abonnement)
  3. Behoud geschiedenis van de laatste 5 bewerkingen (zelfs als laat geabonneerd, zijn de laatste 5 nog steeds zichtbaar)
Voorbeeld van oplossing

1. Gebruikersloginstatus

typescript
import { BehaviorSubject } from 'rxjs';

class AuthService {
  private isLoggedIn$ = new BehaviorSubject<boolean>(false);

  get loginStatus() {
    return this.isLoggedIn$.asObservable();
  }

  login() {
    this.isLoggedIn$.next(true);
  }

  logout() {
    this.isLoggedIn$.next(false);
  }
}

Reden

BehaviorSubject is de beste keuze omdat we de huidige status onmiddellijk bij abonnement nodig hebben.


2. Notificatieberichtlevering

typescript
import { Subject } from 'rxjs';

class NotificationService {
  private notifications$ = new Subject<string>();

  get messages() {
    return this.notifications$.asObservable();
  }

  notify(message: string) {
    this.notifications$.next(message);
  }
}

Reden

Subject is voldoende omdat we het bericht alleen na abonnement hoeven weer te geven.


3. Laatste 5 bewerkingen geschiedenis

typescript
import { ReplaySubject } from 'rxjs';

class HistoryService {
  private actions$ = new ReplaySubject<string>(5); // houd 5 acties bij

  get history() {
    return this.actions$.asObservable();
  }

  addAction(action: string) {
    this.actions$.next(action);
  }
}

Reden

ReplaySubject(5) is de beste manier om de laatste 5 bij te houden en ze op te halen zelfs als je laat abonneert.

Probleem 2: Kiezen Tussen share en shareReplay

In de volgende code, kies de juiste operator.

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

// Scenario 1: Real-time gegevens van WebSocket
const realTimeData$ = webSocket('ws://example.com/stream');

// Scenario 2: API-aanroep voor gebruikersinformatie (we willen de resultaten cachen)
const user$ = ajax.getJSON('/api/user/me');

// Wat moeten we voor beide gebruiken?
Voorbeeldantwoord

Scenario 1: Real-time gegevens van WebSocket

typescript
import { share } from 'rxjs';

const realTimeData$ = webSocket('ws://example.com/stream').pipe(
  share() // geen behoefte om real-time gegevens te cachen
);

Reden

share() wordt gebruikt voor real-time gegevens zoals WebSocket, omdat er geen behoefte is om waarden uit het verleden te cachen. Als je laat abonneert, ontvang je nieuwe gegevens vanaf dat moment.


Scenario 2: API-aanroep voor gebruikersinformatie

typescript
import { shareReplay } from 'rxjs';

const user$ = ajax.getJSON('/api/user/me').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);

Reden

Gebruik shareReplay() om API-resultaten te cachen en te delen tussen meerdere componenten. Gebruik refCount: true om geheugenlekken te voorkomen.

Probleem 3: Geheugenlekken Oplossen

De volgende code heeft een geheugenlek probleem. Los het alstublieft op.

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

const data$ = interval(1000).pipe(
  shareReplay(1) // probleem: dit is hetzelfde als shareReplay({ bufferSize: 1, refCount: false })
);

const sub = data$.subscribe(v => console.log(v));
sub.unsubscribe();

// interval blijft actief na dit → geheugenlek
Voorbeeldoplossing

Aangepaste code:

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

const data$ = interval(1000).pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // refCount: true → Stream stopt wanneer alle abonnementen worden beëindigd
);

const sub = data$.subscribe(v => console.log(v));
sub.unsubscribe(); // stream wordt gestopt

Problemen

  • shareReplay(1) is een afkorting voor shareReplay({ bufferSize: 1, refCount: false })
  • refCount: false betekent dat de stream actief blijft zelfs nadat alle abonnementen zijn beëindigd
  • interval blijft voor altijd actief, wat resulteert in een geheugenlek

Reden voor fix

Met refCount: true, stopt de stream ook wanneer het laatste abonnement wordt beëindigd en wordt de cache gewist.

Probleem 4: Implementatie van Statusbeheer

Implementeer een TodoStore die voldoet aan de volgende vereisten.

Vereisten

  • In staat om Todo-items toe te voegen, te voltooien en te verwijderen
  • Mogelijkheid om todo-lijsten extern op te halen in alleen-lezen modus
  • Mogelijkheid om het aantal voltooide Todo-items op te halen
Voorbeeldoplossing
typescript
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

class TodoStore {
  private todos$ = new BehaviorSubject<Todo[]>([]);
  private nextId = 1;

  // publiceer als alleen-lezen
  get todos(): Observable<Todo[]> {
    return this.todos$.asObservable();
  }

  // Aantal voltooide Todos
  get completedCount(): Observable<number> {
    return this.todos$.pipe(
      map(todos => todos.filter(t => t.completed).length)
    );
  }

  // voeg todo toe
  addTodo(text: string) {
    const currentTodos = this.todos$.value;
    const newTodo: Todo = {
      id: this.nextId++,
      text,
      completed: false
    };
    this.todos$.next([...currentTodos, newTodo]);
  }

  // Todo voltooid
  toggleTodo(id: number) {
    const currentTodos = this.todos$.value;
    const updatedTodos = currentTodos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
    this.todos$.next(updatedTodos);
  }

  // verwijder todo
  removeTodo(id: number) {
    const currentTodos = this.todos$.value;
    this.todos$.next(currentTodos.filter(todo => todo.id !== id));
  }
}

// Gebruik
const store = new TodoStore();

store.todos.subscribe(todos => {
  console.log('Todo lijst:', todos);
});

store.completedCount.subscribe(count => {
  console.log('Voltooid:', count);
});

store.addTodo('Leer RxJS');
store.addTodo('Lees documentatie');
store.toggleTodo(1);

Punten

  • Behoud status in BehaviorSubject
  • Stel het extern bloot als alleen-lezen met asObservable()
  • Gebruik value om huidige status te krijgen en bij te werken
  • Gebruik map om afgeleide status te berekenen (completedCount)

Uitgebracht onder de CC-BY-4.0 licentie.