avatarTomas Trajan

Summary

This article discusses the trade-offs between using the subscribe() method and the async pipe for handling observable data sources in Angular components.

Abstract

The article presents two popular methods for consuming observable streams of state updates in Angular components: the subscribe() method and the async pipe. The author compares the advantages and disadvantages of each method, considering factors such as template readability, subscription management, and compatibility with the OnPush change detection strategy. The author ultimately concludes that the async pipe is the clear winner due to its compatibility with OnPush and automatic subscription management.

Opinions

  • The author was initially in favor of using the subscribe() method but found that it did not work well with the OnPush change detection strategy.
  • The author believes that the async pipe is the best choice for handling observable data sources in Angular components due to its compatibility with OnPush and automatic subscription management.
  • The author suggests breaking complex components into smart wrapper and multiple dumb view components to avoid bloated templates with multiple async pipes.
  • The author recommends using the async pipe as much as possible to simplify application development and ensure one-way data flow.
  • The author acknowledges that the subscribe() method provides some benefits in terms of template readability and simplicity, but only in cases where the OnPush change detection strategy is not being used.
  • The author suggests using a dedicated wrapper element to prevent multiple subscriptions by multiple async pipes in templates.
  • The author recommends using the async pipe to unwrap objects in templates, but notes that this can lead to bloated templates with multiple async pipes.

The Ultimate Answer To The Very Common Angular Question: subscribe() vs | async Pipe

Original 📷 by louis amal

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

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:

  1. Using the subscribe() method and storing the state on the component instance, todos$.subscribe(todos => this.todos = todos)...
  2. The | async pipe 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…

Topics

Please notice following things which make a big impact on the final answer to this question…

  • the way | async pipe 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()

  1. 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
  2. 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.
The only responsibility of the template is to trigger this method (eg using (click)=”toggleAllTodosToDone()”)

👎 Disadvantages of using subscribe()

  1. 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 ngOnDestroy which will not lead to any errors just a silent memory leak…
  2. Subscribing to the observable manually in the ngOnInit() doesn’t work with OnPush change 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

  1. Solution works with OnPush change 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…
  2. Angular handles subscriptions of | async pipes 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

  1. 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".
  2. 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 DOM structure which is pretty weird…
  3. Properties unwrapped in the template using *ngIf or *ngFor are 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…
Multiple subscriptions using | async pipes versus dedicated wrapper element with *ngIf “as” syntax

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 🥊

The 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

Considerations

The 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.

In case of complex template it often make sense to extract parts of it into stand-alone dumb components which can work directly with unwrapped objects while benefiting from the OnPush change detection strategy

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 subscribe() or | async pipe 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

Obviously the bright future (📷 by SpaceX)

If you made it this far, feel free to check out some of my other articles about Angular and frontend software development in general…

JavaScript
Angular
Angular2
Typescript
Rxjs
Recommended from ReadMedium