Async Pipe Is Broken in Angular
In this post you will learn why I think that async pipe inside Angular is broken and how we can fix that.
Inside Angular we are using streams from RxJS a lot to render data inside templates. It is not the best approach to resolve streams with subscribe
inside components. This is exactly why we are using async pipe.
Async pipe subscribes to an Observable or Promise and returns the latest value it has emitted.
Here is my app.component.ts
with a single stream.
export class AppComponent {
currentPage$ = of(1)
}
Here we are using of
method from RxJS to transform pain data in a stream. It doesn't really matter from where we got the value. It can be an HTTP request, Behaviour Subject or something else. The point is that we create here an Observable of number.
And typically when we use async pipe we simply write
{{currentPage$ | async}}
As you can see in the browser 1
is rendered and async pipe works as expected.
Wrong behaviour
But now let's throw this async pipe inside child component.
@Component({
selector: 'child',
template: '<div>{{currentPage}}</div>'
})
export class ChildComponent {
@Input() currentPage: number = 0;
}
<child [currentPage]="currentPage$ | async"></child>
This is the typical code that you will find in all Angular applications.
But now let's look in the console.
Here we are getting a super strange error. We are getting that our type number|null
is not assignable to type number
. It is confusing because inside app.component
we for sure have a value inside currentPage$
which is an Observable of number. When async pipe opens an observable we are getting number. Inside child component we also defined that we get a number.
As this moment you for sure start searching for the problem and find such issue
Which brings understanding that the value of async pipe on initialize is null.
But actually doesn't matter if it's null or undefined on initialize both variants are not nice to handle for us.
I could understand undefined
by default because a stream is a long lasting piece of data but not null
.
The main problem is that in previous versions of Angular we didn't have such problems and the same code worked out of the box and we didn't have such errors.
To understand why we have such problems we much look on the typings inside Angular.
In this type definition we always get null
and undefined
now matter what and we can't change that.
Every time when we use async pipe and provide it inside component you will get null as a value.
Wrong fix
The most obvious fix is to say in the child component
@Input() currentPage: number|null|undefined;
As you can see in terminal it works but this solution is really ugly and we must handle all this cases in our child component.
Good fix
This is why I want to show you another possibility.
<ng-container *ngIf="currentPage$ | async as currentPage">
<child [currentPage]="currentPage"></child>
</ng-container>
Here we used as
notation which creates a local property inside ng-container
. As *ngIf
gets rid of undefined and null we are sure that we get correct value.
As you can see we don't get any errors and our child component can be normally typed.
@Input() currentPage: number = 0;
Handling multiple streams
But now you for sure want to ask "What do we do if we have lots of code and streams?". And you are totally right it looks ugly with lot's of ng-container
all over your files.
We can use combineLatest
to combine several streams together.
export class AppComponent {
currentPage$ = of(1)
foo$ = of('foo')
bar$ = of('bar')
data$ = combineLatest([
currentPage$,
foo$,
bar$
])
}
As you can see here we used combineLatest
to combine our stream in the array. But it is not comfortable to work with it in html. This is why we typically want to transform it to the object.
export class AppComponent {
currentPage$ = of(1)
foo$ = of('foo')
bar$ = of('bar')
data$ = combineLatest([
currentPage$,
foo$,
bar$
]).pipe(map(([currentPage, foo, bar]) => ({
currentPage,
foo,
bar
})))
}
This gets us an stream as an object of data. And this is how we can use it in html.
<ng-container *ngIf="data$ | async as data">
<child [currentPage]="data.currentPage"></child>
<div>{{data.foo}}</div>
<div>{{data.bar}}</div>
</ng-container>
As you can see we wrote just one ng-container
and we can combine any amount of streams that we want.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!