In this Article

This is the first article of the mini-series 'Condensed Angular Experiences'. We will explore concepts in Angular that seem simple but are very powerful and critical for the application architecture.

  1. Understanding Angular's Async Pipe
  2. What is the Hype with OnPush? (coming soon)
  3. Smart vs. Representational Components (coming soon)
  4. Different Approaches to Complex and Advanced Forms in Angular (coming soon)

Every Angular developer knows the async pipe. It is almost always present when writing components and using some observables or promises. This article will explore how it works and for what it is suitable.

You can find more information on RxJs, Observables, and how to use them properly here.

Angular's async pipe is a tool to resolve the value of a subscribable in the template. A subscribable can be an Observable, an EventEmitter, or a Promise. The pipe listens for promises to resolve and observables and event emitters to emit values. Let's take a look at how we can profit from using the async pipe.

Where Do We Profit From Using the Async Pipe?

As simple as the async pipe function sounds, it opens the door to more readable code, fewer unintended memory leaks, and better performance. It makes the code more readable by moving the variable declaration and usage of the template.

<some-component [inputValue]="someObservable$ | async"></some-component>

Further, it handles the subscription for us and, more importantly, unsubscribes from the subscribable if the host component is destroyed.

But the most critical advantage plays into our hands in conjunction with the OnPush change detection strategy. Immutable values bound to a component input automatically refresh the corresponding component's view, but that does not apply to member variables that are used in the template. The async pipe marks the host view as dirty and ready to check if the value was updated.

The OnPush change detection strategy is discussed in detail in the next article of this series.

How Does the Async Pipe Work?

When taking a look at the source code of the async pipe, we can go through its functions step by step and analyze how it works.

The pipe takes a subscribable, subscribes internally to it, stores, and re-emits its value. It also handles the teardown if you swap the underlying object. You can see that behavior in the code below. That is the original implementation, starting with the pipes entry point, the transform function.

// overload
transform<T>(obj: Subscribable<T>|Promise<T>|null|undefined): T|null {
    if (!this._obj) {
      if (obj) {
        this._subscribe(obj);
      }
      return this._latestValue;
    }

    if (obj !== this._obj) {
      this._dispose();
      return this.transform(obj);
    }

    return this._latestValue;
  }

Handling Subscribable and Promises Distinctly

Note that Angular distinguish between a promise and other types. Both types are explicitly handled to their specifications. Both receive the callback that updates the internal value.

private _subscribe(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): void {
  this._obj = obj;
  this._strategy = this._selectStrategy(obj);
  this._subscription = this._strategy.createSubscription(
      obj, (value: Object) => this._updateLatestValue(obj, value));
}

private _selectStrategy(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): any {
  if (ɵisPromise(obj)) {
    return _promiseStrategy;
  }

  if (ɵisSubscribable(obj)) {
    return _subscribableStrategy;
  }

  throw invalidPipeArgumentError(AsyncPipe, obj);
}

The promise variant resolves the Promise via the then function. The Subscribable variant calls the subscribe function instead and returns the subscription. That way, it is possible to unsubscribe later on destroy or calling it directly while swapping the underlying subscription. While the SubscribableStrategy must unsubscribe, the PromiseStrategy does nothing on dispose and on destroy because it doesn't have to. Anyway, the method is called in both cases to be safe and fulfill the API.

The async pipe also unsubscribes inside the on-destroy lifecycle hook to prevent memory leaks. As explained in the first block, the pipe handles the teardown itself by calling the dispose function. Which will call dispose on the specifically selected strategy.

ngOnDestroy(): void {
  if (this._subscription) {
    this._dispose();
  }
}

private _dispose(): void {
  this._strategy.dispose(this._subscription!);
  this._latestValue = null;
  this._subscription = null;
  this._obj = null;
}

This is a simplified schema of the whole process. Notice that the update of the inner value happens asynchronously to the rest.

Async pipe schema

Hooking Into the Change Detection

But the best part is yet to come. The pipe also helps us with the change detection. It can become cumbersome and complex to manage view updates using the OnPush change detection strategy. But the pipe automatically calls the markForCheck() method on updates for us. Afterward, Angular runs the change detection on the next cycle until reaching the host component itself.

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

Now that we know how the built-in async pipe works, I've created a simple demo app that shows the different scenarios and effects of the async pipe in conjunction with the OnPush change detection strategy.

The Async Pipe in Action

Using the pipe is as simple as this:

<some-component [inputValue]="someObservable$ | async"></some-component>

It takes the observable and maps its value to the input.

Alternatively, we could resolve it in an *ngIf and access the object's properties.

<ng-container *ngIf="someObservable$ | async as resolved">
    <some-component [inputValue]="resolved.value"></some-component>
    <other-component [data]="resolved.dataset"></other-component>
</ng-container>

All that works fine until we must handle mutable objects. Angular's change detection works by comparing the identity of two objects obj1 === obj2 and Object.is(), disregarding internal changes. However, this does not work with a mutable object, but how does it come then that it seems to work in the demo application (as seen on the lower right card)?

As described, the pipe marks the host component to be checked in the next change detection cycle. That check will go through the component's view and looks for changes. That way, even, at first, undetected changes will be updated.

If you can't use the async pipe for some reason, you could, for instance, resolve observables and promises in your component.

An Alternative to the Async Pipe

As alternatives to the async pipe, you can resolve the observable or promise in your component.

this.valueSub = this.numberService.value$.subscribe(x => {
    this.value = x;
    this.changeDetectorRef.markForCheck();
});

//...

But keep in mind, you have to make sure not to cause any memory leaks by unsubscribing from the observable.

ngOnDestroy(): void {
    this.valueSub.unsubscribe();
    // ...
}

When using OnPush you also have to make sure that the view is updated correctly. For this, we can call the markForCheck() method on the ChangeDetectorRef and notify it about changes.

this.valueSub = this.numberService.value$.subscribe(x => {
    //...
    this.changeDetectorRef.markForCheck();
});

Conclusion

This article explores the async pipe, how it works, what it is used for, and examines its source code in detail. We have seen how to use it and provided examples, demos, and alternatives. Now it is up to you to decide where to use it in your projects and when. This conclusion might help you further with that decision.

As seen in the examples above, we don't have to use Angular's build-in async pipe. We can resolve promises and subscribable ourselves. But we must handle the subscription clean-up and make sure the view is updated. The latter part is vital in conjunction with the OnPush change detection strategy.

By using the async pipe, we let Angular take on the heavy lifting for us. It simplifies the components and reduces written code by moving the complexity into the framework. It also unifies the way we handle Promises, Observables, EventEmitter, and Subjects; generally Subscribables - creating a pleasant dev experience at the cost of increasing the required framework knowledge. Isn't it what we want a Framework for to hide complexity and for a good consistent API? But we don't have to add extra imports to our components and write repetitive code. We don't have to worry about changes when using the OnPush change detection strategy if we use immutable objects or cut the components accordingly.

If you do not want to miss out on new articles, sign up for our free, monthly Dev Newsletter. Find more information on the OnPush change detection in the upcoming article of this condensed Angular experience. Get notified once it is published.

Related Articles

 | Max Schulte

This is the second article of the mini-series "Condensed Angular experiences". We will explore concepts in Angular that seem simple but are very powerful and critical for the application architecture. Understanding Angular's Async pipe What is the hype with Angular's OnPush…

Read article
 | Yannick Baron

Should You Use NgRx, in the First Place? This is a question that cannot be answered easily. Like every framework or library out there, it was made to do a certain thing, and to do it well. If your app is tiny and does not need an elaborate state management system, why bother. You…

Read article
 | Konstantin Denerz

In this article you will see how to enrich your test suite with functional and visual end-to-end tests for your Angular applications. Setting the Scene In the previous article, Visual Testing Of Angular Components With Storybook & Chromatic, I described how to use visual tests to…

Read article