Home > OS >  Avoiding nested Subscriptions. Subscribe once a different subscription is ready?
Avoiding nested Subscriptions. Subscribe once a different subscription is ready?

Time:11-15

Let's assume that my Angular 14 app has a FireService that will fetch the latest information for an object when the application loads. A page on the application must wait for the Observable on the FireService to be ready before it can do whatever it needs to. This is what I'm currently doing, and I'm sure is wrong:

fire.service.ts

export class FireService {
  public thingA$ = new BehaviorSubject<ThingA | undefined>(undefined);
}

page.component.ts

this.FireService.thingA$.subscribe((a: ThingA | undefined) {
  if (a) {
    this.thingB$.subscribe((b: ThingB) => doThingWithB(b));
  }
});

Would I use a skipWhile like this?

this.FireService.thingA$.pipe(skipWhile((a: ThingA | undefined) => !a)).subscribe((a: ThingA) {
  if (a) {
    this.thingB$.subscribe((b: ThingB) => doThingWithB(b));
  }
});

Wouldn't this also be a nested subscription?

CodePudding user response:

Here are several things that you can do that will improve your code a lot.

You had the right idea and were on the right path.

you can use the helper functions filter to filter the emissions that you don't like, and concatMap to concatenate the first emission to the second emission.

You can also use skipWhile but in this case I think filter is more descriptive, but feel free to leave the other one.

this.FireService.thingA$.pipe(
  filter((a: ThingA | undefined) => a != null),
  concatMap(() => this.thingB$),
).subscribe({
  // note that this notation is preferred in latest rxjs versions
  next: (b: thingB) => doThingsWithB(b),
  error: (e) => handleTheError(e),
});

Also, you need to add takeUntil so this BehaviorSubject closes before component destroy (otherwise you get memory leaks).

So you would end up with something like:

// on top of the component
private this.destroy$ = new AsyncSubject<null>();

// where you sub to this:
this.FireService.thingA$.pipe(
  filter((a: ThingA | undefined) => a != null),
  concatMap(() => this.thingB$),
  takeUntil(this.destroy$)
).subscribe({
  // note that this notation is preferred in latest rxjs versions
  next: (b: thingB) => doThingsWithB(b),
  error: (e) => handleTheError(e),
});

// now adding on destroy
ngOnDestroy() {
  this.destroy$.next();
  this.destory$.complete(); // this triggers the takeUntil
}

Ignorable suggestions:

There are more improvements I'd suggest to you. The first one, is removing logic from the subscribe method and leaving that empty. There are two operators that can do this (I prefer the second option):

tap let's you create side effects without affecting the outcome, while map allows you to modify what the subscribe method will get.

So, if your doThingsWithB function is a side effect, you can call it with tap and it would look like this:

this.FireService.thingA$.pipe(
  filter((a: ThingA | undefined) => a != null),
  concatMap((a: ThingA) => this.thingB$),
  tap((b: ThingB) => doThingsWithB(b)),
).subscribe();

which is more readable. Can also allow you to leave this observable in a variable, and call the subscribe later.

Also, if you usually use the data that comes from the observable, like assigning it to a value that is passed to the template, you can map it and use async pipe. So instead of

thingB = undefined;
this.thingB$.subscribe(b => thingB = makeCalculationWithB(b.prop));

<div>{{ thingB }}</div>

You can just map it, and use the async pipe.

thingB = this.thingB$.pipe(map(b => makeCalculationWithB(b));

<div>{{ thingB | async }}</div>

which would handle the unsubscribe as well, which is an added bonus (no need to takeUntil or ngOnDestroy). Also allows for easier extension of the logic in the future, becase you just add the pipes to it.

CodePudding user response:

If you want to call them in parallel, and thingB is not dependent on thingA result, then:

forkJoin(
    this.FireService.thingA$(),
    this.FireService.thingB$()
).subscribe(
    ([thisngA, thingB]) => {
        // do your things
    },
    (error) => console.error(error)
)
  • Related