- Understanding Angular’s Async pipe
- What is the hype with Angular’s OnPush change detection?
- Interlude: About smart and representational components
- Smart and presentational components with Angular
- Different approaches to complex and advanced forms in Angular (coming soon)
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.
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(obj: Subscribable|Promise|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|Promise|EventEmitter): 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|Promise|EventEmitter): 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:
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.
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.