Let's say I have two observables to be consumed by the template, one with the data and one as a loading indicator:
public readonly data$ = this.backend.get<Data>(...).pipe(
finalize((
{
this.isLoadingSubject$.next(1);
return () => this.isLoadingSubject$.next(-1);
})()
);
private readonly isLoadingSubject$ = new Subject<number>();
public readonly isLoading$ = isLoadingSubject$.pipe(
scan((acc, value) => acc + value, 0),
map((i) => i > 0),
);
Explanation on the finalize: I'm using an IIFE to start loading when the pipeline of data$ gets triggered, and to end loading when it completes. I am using a number that gets incremented or decremented instead of a boolean because there might be multiple simultaneous requests (the isLoading$ mechanism is used by many observable pipelines).
In my template I use it like this:
<ng-container *ngIf="data$ | async as data">
{{ data }}
<button [disabled]="isLoading$ | async">some button</button>
</ng-container>
The problem is that the subscription to isLoading$ is late: this subscription only happens once data$ has emitted, and the first .next(+1) gets ignored because there are no subscribers.
How do I solve this elegantly?
Workarounds I have tried and that I do not like:
- Subscribing to
isLoading$immediately to make it hot - seems wasteful, and when reading the code, it's not apparent why this is done. Because of that this seems like a bad workaround if it's not clear what it is for from the code alone. - Rearranging the template so that
isLoading$is in a first<ng-container>and thendata$in a second<ng-container>- but then I have to deal with*ngIfnot rendering the template when loading isfalse, so I have to wrap it in an object, which seems wasteful again. And also, this causes everything to be re-rendered every time the loading toggles, which is stupid. - Looked at the
publishReplay()operator, but that's deprecated. - Wrapping both
data$andisLoading$in an object inside the same<ng-container>, but then the whole template gets re-rendered whenever the loading indicator changes, this is super wasteful - I only want to disable a button.