1

Angular 11 site. I've searched all over but I'm still struggling with what seems like it should be a pretty common scenario.

Goal: Do an http call to retrieve a list of items, then do another http call to retrieve details for the first item. After both calls are done (whether they succeeded or failed), hide the component's overall wait indicator.

Here is a highly simplified version without null-checking and error handling:

  ngOnInit(): void {
    this.getPackages();
  }

  private getPackages() {
    this._packageSubscription = this.packageService.getPackages()
      .pipe(
        finalize(() => {
          console.log('getPackages finalize');
          this.loadingPackages = false; //hide wait indicator: happens BEFORE getPackageDetails is finished
        })
      )
      .subscribe(
        packages => {
          this.packageOptions = packages;
          this.selectedPackage = packages[0];
          this.loadSelectedPackage();
        }
      );
  }

  public loadSelectedPackage() {
    this._packageDetailsSubscription = this.packageService.getPackageDetails(this.selectedPackage!.Id)
      .pipe(
        finalize(() => {
          console.log('getPackageDetails finalize');
        })
      )
      .subscribe(
        packageDetails => {
          this.packageDetails = packageDetails;
        }
      );
  }

When executed, the console reads:

getPackages finalize
getPackageDetails finalize

I would expect it the other way around.

I'm aware I could add my code to deactivate the wait indicator in the inner finalize() for getPackageDetails(), but there are a few issues with that:

  1. It just seems wrong. Why can't I know when the chain is complete at the outer level?
  2. That would only cover the case where everything succeeded. I'd have to add a second line of code somewhere to hide the wait indicator when something with the first call failed.
  3. loadSelectedPackage() can be called again later after a user action changes the selected package, so it makes even less sense for it to deal with something that is only relevant on the initial load.

So why doesn't my outer finalize() wait until the chain is complete? Or how else can I know when the chain is complete?

Roobot
  • 860
  • 1
  • 10
  • 20

1 Answers1

3

It's actually the correct result you are getting. The way you wrote you code there is no chain. Those two subscriptions are independent of each other, by calling this.loadSelectedPackage(); from within the subscribe you are not concatenating them or anything like that.

You should actually use some map operator and subscribe only once. In your case I believe concatMap() is suitable. Let the getPackages() happen, assign the values to object properties and then concatenate new observable to get details.

private getPackages() {
  this._packageSubscription = this.packageService.getPackages()
    .pipe(
      concatMap(packages => {
        this.packageOptions = packages;
        this.selectedPackage = packages[0];
        return this.packageService.getDetails(this.selectedPackage!.Id);
      }),
      finalize(() => {
        console.log('getPackages finalize');
        this.loadingPackages = false; 
      })
    )
    .subscribe(packageDetails => {
      this.packageDetails = packageDetails;
    });
}

This should do the trick.

mat.hudak
  • 2,803
  • 2
  • 24
  • 39
  • Thanks, I will look at this, but my question is, if you say I'm not chaining them, then why does the second call use the correct ID from the first result of the first call? It definitely waits until the first call is done to execute the second. – Roobot Mar 25 '21 at 18:32
  • 1
    That's because you are calling the `this.loadSelectedPackage()` from subscribe. This means, that `getPackages` is finished, you have the data, you have the ID and the observable is closed. And from there you are calling synchronous function which then subscribes to `loadSelectedPackages()`. That's what I meant that they are not chained. – mat.hudak Mar 25 '21 at 18:49
  • But if you move `this.loadingPackages = false;` to the finalize of `getPackageDetails()` you'll get the desired effect. Data loaded and wait indicator disappears when it should. – mat.hudak Mar 25 '21 at 19:00
  • This worked, thanks. I was looking to call my `getPackages()` function onInit and chain that to my `loadSelectedPackage()` function for the first package, then also have `loadSelectedPackage()` called when the user changed it. But it seems I really need to chain http calls more directly. I guess I will have to refactor some other stuff (there's more that happens with the http results than just setting one property like I've done here and that should happen regardless of if it's loading the first package details or a subsequent one), but that should work. Thanks again. – Roobot Mar 25 '21 at 19:35