Async Pipe Is Broken in Angular
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.

Error

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

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.

Typings

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.

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.