Most of the popular Angular state management libraries like NgRx expose application state in a form of a stream of state objects. This is usually implemented with the help of RxJS Observables.
The state updates get pushed to the components which react by re-rendering their templates to display the most recent version of the application state.
There are multiple ways in which it is possible to consume this observable stream of state updates by the components, the two most popular being:
- Using the
subscribe()method and storing the state on the component instance,
todos$.subscribe(todos => this.todos = todos)...
| asyncpipe unwraps the state object directly in the component’s template,
<li *ngFor=”let todo of todos$ | async”></li>...
I have been thinking about this question and related trade-offs for quite some time now. I was mostly in favor of using subscribe() but couldn’t really point out exact reasons why.
This led to a need to come up with an overview of the situation while considering all the pros and cons to create a final guideline to be able to make objective decision in every case!
Before we dig deeper, I would like to thank simply10w and Tim Deschryver who provided lots of great input and feedback (PR) while working on Angular NgRx Material Starter project which implements ideas discussed in this article…
Please notice following things which make a big impact on the final answer to this question…
- the way
| asyncpipe works for the collection vs for singular objects
- possibility of using new-ish
*ngIf“as” syntax (from Angular 4 and above)
- location of state handling logic (component’s class vs template)
OK, let’s do this!
Case 1: Use subscribe() in the ngOnInit method
👍 Advantages of using subscribe()
- Unwrapped property can be used in multiple places in the template “as it is” without the need to rely on various workarounds as will be shown in the case 2 examples
- Unwrapped property is available everywhere in the component. This means it can be used directly in the component’s method without the need to pass it from the template. That way, all state can be kept in the component.
👎 Disadvantages of using subscribe()
- Using of the
subscribe()introduces complementary need to unsubscribe at the end of the component life-cycle to avoid memory leaks. Developers usually have to unsubscribe manually. The most RxJS (declarative) way to do this is to employ
takeUntil(unsubscribe$)operator as shown in the example above. This solution is verbose and error prone because it is very easy to forget implementing
ngOnDestroywhich will not lead to any errors just a silent memory leak…
- Subscribing to the observable manually in the
ngOnInit()doesn’t work with
OnPushchange detection strategy out of the box. We could make it work by using
this.cd.markForCheck()inside of our subscribe handler but this is a very easy to forget, error prone solution.
The issue with the OnPush change detection strategy was the final straw and the deal-breaker for my previously favorite “subscribe()” approach to handling of the observable data sources in the Angular components
Case 2: Use | async pipe in the component template
👍 Advantages of using | async pipe
- Solution works with
OnPushchange detection out of the box! Just make sure that all your business logic (eg reducer, service) is immutable and always returns new objects. Anyway, this is the whole purpose of using NgRx in a first place so I guess immutable data can be assumed…
- Angular handles subscriptions of
| asyncpipes for us automatically so there is no need to unsubscribe manually in the component using
ngOnDestroy. This leads to less verbosity and hence less possibilities for making a mistake. Yaaay 😸
Follow me on Twitter to get notified about the newest Angular blog posts and interesting frontend stuff!🐤
👎 Disadvantages of using | async pipe
- Objects have to be unwrapped in the template using
*ngIf="something$ | async as something"syntax. On the other hand, this is not a problem with collections which get unwrapped in a straight forward manner when using
*ngFor="let something of somethings$ | async".
- Objects have to be potentially unwrapped multiple times in a single template in multiple different places. This can be avoided by using a dedicated wrapper element but that means that the state management is mandating changes to
DOMstructure which is pretty weird…
- Properties unwrapped in the template using
*ngForare not accessible in the component’s methods. This means we have to pass these properties to the methods from the template as the method parameters which further splits logic between the template and the component itself…
We could also use
<ng-container> instead of
<div>. This way, no new wrapper element will be created in the final
DOM but we still end up with wrapped element in the template source code.
Many thanks to Martin Javinez for suggesting solution with multiple
| async pipes resolved into one variable…
Many thanks to Ward Bell for pointing out that besides using wrapper
<ng-container></ng-container> element there is an another way of preventing multiple subscription by multiple | async pipes in our templates…
The solution is to pipe our observable stream through
ReplaySubject like this
something$ = sourceOfSomething$.pipe(ReplaySubject(1));
The Verdict 🥊
OnPush change detection strategy is great for performance so we should be using
| async pipe as much as possible. More so, we could argue that the consistent use of these features simplifies application because developers can always assume one way data flow and automatic subscription management.
The | async pipe is the clear winner
subscribe() solution does provide some benefits in terms of template readability and simplicity. It can be the right solution in case we’re not using
OnPush change detection strategy and are not planing to use it in the future…
Also, check out related great tip from Tim Deschryver. If we find ourselves in a situation when our template is getting too complex and bloated with many
| async pipes unwrapping a lot of objects we should consider breaking that component into a smart wrapper and multiple dumb view components…
After that you can simply pass unwrapped property inside your dumb component like this
<dumb [prop]="value$ | async"></dumb> so you have working
OnPush while having a benefit of working with the unwrapped objects in the potentially complex template of the dumb component.
That’s It For Today!
I hope you enjoyed this article and will now have a mental model that helps you to decide whether to use
| asyncpipe in the components of your Angular applications!
Please support this guide with your 👏👏👏 and help it spread to a wider audience 🙏. Please, don’t hesitate to ping me if you have any questions using the article responses or Twitter DMs @tomastrajan
And never forget, future is bright
If you made it this far, feel free to check out some of my other articles about Angular and frontend software development in general…