Understanding Angular’s Async Pipe: Condensed Angular Experiences – Part 1

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.

In diesem Artikel:

mm_300x300
Max Schulte ist Consultant bei Thinktecture mit dem Schwerpunkt auf Angular und 2D/3D Visualisierung.
  1. Understanding Angular’s Async pipe
  2. What is the hype with Angular’s OnPush change detection?
  3. Interlude: About smart and representational components
  4. Smart and presentational components with Angular
  5. 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.

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.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Angular
sl_300x300

View Transition API Integration in Angular—a brave new world (Part 1)

If you previously wanted to integrate view transitions into your Angular application, this was only possible in a very cumbersome way that needed a lot of detailed knowledge about Angular internals. Now, Angular 17 introduced a feature to integrate the View Transition API with the router. In this two-part series, we will look at how to leverage the feature for route transitions and how we could use it for single-page animations.
15.04.2024
Low-angle photography of metal structure
AI
cl-neu

AI-Funktionen zu Angular-Apps hinzufügen: lokal und offlinefähig

Künstliche Intelligenz (KI) ist spätestens seit der Veröffentlichung von ChatGPT in aller Munde. Wit WebLLM können Sie einen KI-Chatbot in Ihre eigenen Angular-Anwendungen integrieren. Wie das funktioniert und welche Vor- und Nachteile WebLLM hat, lesen Sie hier.
26.02.2024
Angular
sl_300x300

Konfiguration von Lazy Loaded Angular Modulen

Die Konfigurierbarkeit unserer Angular-Module ist für den Aufbau einer wiederverwendbaren Architektur unerlässlich. Aber in der jüngsten Vergangenheit hat uns Angular seine neue modullose Zukunft präsentiert. Wie sieht das Ganze jetzt aus? Wie konfigurieren wir jetzt unsere Lazy-Komponenten? Lasst uns gemeinsam einen Blick darauf werfen.
03.08.2023
Angular
YB_300x300

Using EntityAdapter with ComponentStore: @ngrx/entity Series – Part 3

As someone who enjoys the ComponentStore on an average level, I have written simple reactive CRUD logic several times. While storing a vast number of entities in the component state might not be a frequent use case, I will briefly illustrate the usage of the EntityAdapter with the @ngrx/component-store.
14.02.2023
Angular
YB_300x300

Multiple Entity Collections in the Same Feature State: @ngrx/entity-Series – Part 2

After introducing the @ngrx/entity package, I am often asked how to manage multiple entity types in the same feature state. While I hope that the previous part of this article series has made this more apparent, I will further focus on this question in the following.
07.02.2023
Angular
YB_300x300

Managing Your Collections With the EntityAdapter: @ngrx/entity-Series – Part 1

This three-part series of blogposts is targeted at developers who have already gained experience with NgRx but still manage their collections themselves. In the first part I introduce the Entity Adapter, in the second part I show you how to connect it to NgRx and in the third part how to do it with the Component Store as well.
31.01.2023