Presents the observables

Many concepts in Angular are essentially observables, such as:

  • Returns using the httpClient Call API
  • The URL params or queryParams obtained using the ActivatedRoute
  • To use Angular Forms, you need to get the Form changes, valueChanges
  • Use the Output EventEmitter in Component

Of course, there are a few other concepts that we wish were an Observable, but are not, or are not well supported:

  • Component (NgChanges => Observable) @input is an Observable, @output is an Observer, Bidirectional binding is essentially a Subject.
  • @Input in Component replaces EventEmitter directly with Subject (currently compatible).
  • Component lifecycle functions => Observable, for example, ngDestroy. This eliminates the need to split the logic into different lifecycle functions.
  • The Template support is not good enough.
  • Events to Observable are tricky to define.

The Async Pipe in Angular

Since many concepts in Angular are already To Obserable, the logic is much simpler if you can use an Observable directly in the Template. Here’s the simplest example:

There is an API /users/:userId that returns specific information about the User. The User ID is read from the URL and the specific information is displayed on the page. The logic is simple:

const user$ = this.activeRoute.params.pipe( distinctUntilChanged(params => params.id), switchMap(id => { return this.service.getUser(id); }));Copy the code

Page data display is also relatively simple, just subscribe and assign to a public variable.

user$.subscribe(user => this.user = user);

{{ user | json}}
Copy the code

Of course, there are some other problems, like you have to define a user variable again, like you need to write more mindless subscribe. For example, you need to consider whether you need to unsubscribe.

Can we assign mindless subscribe and unsubscribe and duplicate variables to the framework layer? The answer is async pipe.

{{ user$ | async | json }}
Copy the code

You can use it elsewhere in the template:

<div *ngIf="user$ | async as user"> {{user? .name}} </div>Copy the code

This way, not only do you not need to define a global variable with the same name as user$(you can, of course, divide observable and normal value with or without $). Angular province also automatically unsubscribe when the Component is destroyed. It saves a lot of repetitive work.

Async can also be used in *ngFor or Component input, for example:

<div *ngFor="let user of users$ | async"> {{user? .name}} </div>Copy the code

Or:

<app-user [userInfo]="user$ | async"></app-user>
Copy the code

And that actually solves most of the cases where you have to subscribe and then bind to a template.

Angular dirty check and Change Detection

If you are familiar with Angular or its predecessor, AngularJS, you should be familiar with dirty checking. To put it simply, how to notify the page when the data in the Component with the Template bind changes, for example:

// Template
{{user | json}}

// Component.ts 
setTimeout(() => {
  this.user = {};
}, 3000);
Copy the code

How Does Angular know that the HTML of the current component will be updated when the user changes three seconds later? The answer is a dirty check. The simplest example would be, for example, to check every component field once in a while for any changes, and then re-render the HTML once it changes. Of course, the real situation is much more complicated. To save resources, Angular checks for @Input changes, click Events, setTimeout, etc.

This is what we call a dirty check.

So, what does this have to do with async? In Angular, though, dirty checking is no longer a performance-intensive affair, and there is no need to wrap JS functions around dirty checking. But we think that Angular, by default, is always trying to minimize unnecessary checks and get closer to reality. Is it possible that we actively notify Angular when data changes in the Template? Otherwise, Angular doesn’t check.

This is called ChangeDetectionStrategy, onPush, which only checks for data changes when necessary, i.e. :

  • When the Input changes
  • Event or at sign Output Event occurs
  • Emit value in the stream corresponding to the Async pipe
  • When actively Detect changes

In this way, dirty checks can be minimized, similar to Push Detect Changes in Vue. In principle, we can bind only two types of data to the Template:

  • Data that changes based on Input or Event
  • Bind data via Async

Therefore, another important feature of Async is to optimize performance with the ChangeDetectionStrategy implementation to get rid of Angular dirty checking.

The actual use of Async Pipe

In fact, Angular only provides Async pipes for Observables, and using them in templates is very limited and has a lot of pitfalls. Here’s an example:

{{user$ | async}}
<app-user [userInfo]="user$ | async"></app-user>
Copy the code

This is a common error with async when you’re new to async, and it’s easy to ignore because it’s perfectly fine to change user$to a normal value. But the stream data user$is completely different, because each async pipe actually executes a subscribe. According to the previous article, the same Observable subscribes multiple times independently of each other. Consider the example above:

this.user$ = this.httpClient.get('/user/xxx');
Copy the code

You’ll notice that Ajax calls happen multiple times. Of course, there are many solutions. You can change this.user$.pipe(share()) to turn a cold Observable into a hot one.

Personally, I recommend not subscribing to streams manually in component and not async pipe streams more than once.

Implementation method is also hate simple, is as far as possible the async | in more outer pipe. For example, the above example could be implemented like this:

<ng-container *ngIf="user$ | async as user">
  {{user}}
  <app-user [userInfo]="user"></app-user>
</ng-container>
Copy the code

If you are careful, you may notice another problem. The two are not completely equivalent. Before user$emit value, the part of the package will be in ngIf flase, that is, the HTML will disappear, which is often not our intention. The purpose of the Async Pipe is just to stream data, but we don’t want the scope it wraps to affect the display. Essentially what we need to leverage is the scope of the variable.

Unfortunately, Angular doesn’t have a single,*ngIf.*ngFor.@Input pipeOther methods. Of course, a simple hack could do that, for example, by making ngIf never befalse. The simplest is to wrap our values in an Object, which is the worst case{}, so as to achieve non-null. Here’s an example:

<ng-container *ngIf="{ user: user$ } as state">
  {{state.user}}
  <app-user [userInfo]="state.user"></app-user>
</ng-container>
Copy the code

Of course, you can also use the ngrxLet provided in NGRX/Component for a more concise look:

<ng-container *ngrxLet="user$ as user">
  {{user}}
  <app-user [userInfo]="user"></app-user>
</ng-container>
Copy the code

So the second lesson is: if you only want to useStreaming dataNot wanting to affect HTML, you can use non-false*ngIfOr usengrxLet.

Of course, this is unlikely to be the last pit, and there are many others. In practice, we tend to have more than one stream of data, and templates can be tricky to use. Also, *ngIf cannot be applied twice on the same HTML tag. There are three ways to handle complex Async Pipes:

  • usingng-containerFeatures that do not occupy HTML space:
  <ng-container *ngIf="a$ | async as a">
  	<ng-container *ngIf="b$ | async as b">
  		<ng-container *ngIf="c$ | async as c">
  			<div ngIf="a && b &&c">xxxx</div>
  		</ng-container>
  	</ng-container>
  </ng-container>
Copy the code

Of course, this makes simple code look more complicated, even though the resulting HTML is still:

XXXX
  • using*ngIfImplement multipleThe data flowPackage:
<ng-container *ngIf="{a: a$ | async, b: b$ | async, c: c$ | async} as state">
  <div ngIf="state.a && state.b &&state.c">xxxx</div>
</ng-container>
Copy the code

In fact, we can conclude a similar solution, which is to wrap a layer of Ng-container in the outermost layer of component as component state to handle all data.

  • There is, of course, a similar trick to include all in componentStreaming datathroughcombineLatestTo create a similar effect. (Of course, it’s not exactly the same, so let’s leave it at that). Of course, I think it’s most convenient to implement a simple state method.
  • Although,*ngIfYou can’t write multiple Async Pipes in a row, but Component fully supports multiple input Async Pipes, so it’s a good idea to split component as much as possible to implement multiple Async Pipes through component’s input.