Let's say I have an observable called 'todoList$'. Using the 'async' operator, I'm able to automatically subscribe/unsubscribe from it. The problem in the code below is that there are two identical subscriptions to the same observable:
<ng-container *ngIf="(todoList$ | async).length > 0>
<div *ngFor="let todo of todoList$ | async">
...
This isn't very DRY, and consequentially, we allocate memory for a subscription that could handled more efficiently.
Because of the syntax in the ngIf condition, I don't believe I can use the 'as' keyword to create a template variable for the observable output. Instead what works is when I use the RxJs 'share' operator from the component file:
todoList$ = this.store.select(todoList).pipe(tap(x => {console.log('testing')}), share());
//testing
Without the share operator, "testing" is printed twice. This leads me to believe the share() operator solves this problem. If it does, not exactly sure why/how? Since this can be a prevalent issue/ code smell, what's the best way of handling multiple subscriptions that are identical within the same template?
I acknowledge there are a few flavors of a similar question floating on StackOverflow. But none have given me exactly what I'm looking for.
As a general rule I use the shareReplay({ refCount: true, bufferSize: 1 }) operator at the end of every Observable inside my template. I also add it to base observables which I use to branch of other observables which are then used in the template.
This will make sure subscriptions are shared among every subscriber, and by using the shareReplay you can get the last emitted result as well inside your component by using take(1).
The reason of the { refCount: true, bufferSize: 1 } is that if you just use shareReplay(1) it can cause leaking subscriptions, regardless if you are using the async pipe.
Back to your example, the answer provided by Michael D is not bad, and it makes sense to do it that way. However, it does require some logic in the template, which I personally frown upon.
So, as long as you are using shareReplay, there is really no downside into using multiple async calls in your template, and you can even make them descriptive and re-usable throughout your template by defining them in your component:
export class TodoComponent {
readonly todoList$ = this.store.select(todoList).pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
readonly hasTodos$ = this.todoList$.pipe(
map((todos) => todos?.length > 0),
shareReplay({ refCount: true, bufferSize: 1 })
);
}
You can then keep your template descriptive:
<ng-container *ngIf="hasTodos$ | async>
<div *ngFor="let todo of todoList$ | async">
<!-- -->
don't forget your trackBy!
If you dislike repeating your code, you can even create your custom operator, and use that:
export function shareRef<T>() {
return (source: Observable<T>) => source.pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
}
Which changes your observable to:
readonly todoList$ = this.store.select(todoList).pipe(
shareRef()
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With