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)
)