Bahaa
Bahaa

Reputation: 1747

Unexpected behavior of RxJS operators zip, merge, forkJoin and concat

In this StackBlitz ToDo application, I'm writing a simple 3 column presentation based on chaining observables.

In the AppComponent, my observables are declared and initialized as follows:

import { Component, OnInit } from '@angular/core';
import { forkJoin, map, merge, Observable, zip } from 'rxjs';
import { DataService } from './data.service';
import { Item } from './item';
import { State } from './state';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  protected allItems: Observable<Item[]>;
  protected pendingItems: Observable<Item[]>;
  protected inProgressItems: Observable<Item[]>;
  protected doneItems: Observable<Item[]>;
  protected cancelledItems: Observable<Item[]>;
  protected terminalItems: Observable<Item[]>;

  constructor(protected _dataSvc: DataService) {
    this.allItems = this._dataSvc.items.asObservable();
    this.pendingItems = this.allItems.pipe(
      map((data) => data.filter((p) => p.status === State.Todo))
    );
    this.inProgressItems = this.allItems.pipe(
      map((data) => data.filter((p) => p.status === State.Doing))
    );
    this.doneItems = this.allItems.pipe(
      map((data) => data.filter((p) => p.status === State.Done))
    );
    this.cancelledItems = this.allItems.pipe(
      map((data) => data.filter((p) => p.status === State.Cancelled))
    );
    // First option
    this.terminalItems = merge(this.doneItems, this.cancelledItems);
    // Second option
    //this.terminalItems = forkJoin([this.doneItems, this.cancelledItems]).pipe(
    //  map(d => d[0].concat(d[1])));
    //);
    // Third option
    //this.terminalItems = concat(this.doneItems, this.cancelledItems);
    // Fourth option
    //this.terminalItems = zip(this.doneItems, this.cancelledItems)
    //  .pipe(map(d => d[0].concat(d[1])));
  }
  
  ngOnInit(): void {
    this._dataSvc.initData();
  }
}

As you can see in the InMemoryDataService, the field allItems gets the following content

let items: Item[] = [
      {id:1, name: "Eat", status: State.Todo},
      {id:2, name: "Sleep", status: State.Todo},
      {id:3, name: "Code", status: State.Todo},
      {id:4, name: "Game", status: State.Doing},
      {id:5, name: "Swim", status: State.Done},
      {id:6, name: "Bike", status: State.Cancelled},
    ];

The RxJS operator decision tree guided me to use merge to initialize the observable terminalItems as follows:

this.terminalItems = merge(this.doneItems, this.cancelledItems);

In app.component.html, I expected the Terminal div, to contain items of status Cancelled and Done (i.e. Bike, Swim).

However, the Terminated column contains only cancelledItems. Changing the order of the arguments of the merge operator makes the doneItems appear, but not the cancelledItems.

Checking the documentation and trying alternatives (forkJoin, concat and zip), I tested how different operators produce terminated items and found the following:

Could someone explain why only zip is working as expected? although according to the documentation, merge looks like it must work!

Upvotes: 0

Views: 330

Answers (2)

Mark van Straten
Mark van Straten

Reputation: 9425

Merge will merge the emissions of n Observables. So you get two Item[] values, every one which will replace the previous and your ui will render the names in that array.

You can see this behaviour when you add a delay(2000) to the doneItems which will make that emission come later and overwriting the render.

To get the combined values you will have to accumulate whichever items are already there with new emissions into one array using combineLatest

Upvotes: 1

Chris Hamilton
Chris Hamilton

Reputation: 10946

You need to use combineLatest since forkJoin only fires when all observables complete.

    this.terminalItems = combineLatest([
      this.doneItems,
      this.cancelledItems,
    ]).pipe(map(([a, b]) => [...a, ...b]));

Working stackblitz: https://stackblitz.com/edit/angular-ivy-1qguvv?file=src/app/app.component.ts

More info: https://www.learnrxjs.io/learn-rxjs/operators/combination/combinelatest

You might prefer learnrxjs.io to the official documentation, I find it more helpful.

Upvotes: 2

Related Questions