Hello, everyone. Here are some of the pitfalls you encounter in Angular development. Hope you can avoid them perfectly

The premise

To understand

  • Angular async pipe
  • Observable
  • RXJS throttling function debounceTime
  • Jasmine Async + Tick

The official explanation for Async pipe is that the async pipe subscribes to an Observable or Promise and returns the last value it emitted. When a new value arrives, the Async pipeline marks the component as needing change detection. When a component is destroyed, the Async pipeline automatically unsubscribers to eliminate potential memory leaks.

FakeAsync + Tick test in Jasmine: fakeAsync will create a fake zone that the tick will use in fakeAsync to simulate an asynchronous time-lapse timer.

Say first conclusion

Use the async pipe only once for the same Observable or Promise in the template. Otherwise, Angular creates the same monitor event Subscription, consuming memory and reducing performance.

If need to repeatedly use the value of the subscription in observables or Promise, you can use * ngIf = “someObservable | async as someValue method will be issued its latest one value is converted to a temporary variable someValue, This temporary variable can be used directly in child nodes.

For example, the original code wanted to listen for prop1 and prop2 in someObservable in real time and display them on the page:

<div> 
    {{ (someObservable | async).prop1 }} - {{ (someObservable | async).prop2 }}
</div>
Copy the code

This code creates 2 subscriptions to listen to someObservable.

Refactored code with *ngIf:

<div *ngIf="someObservable | async as someValue"> 
    {{ someValue.prop1 }} - {{ someValue.prop2 }}
</div>
Copy the code

The code modified above creates 1 Subscription, reducing memory consumption. But div is not created if someObservable never emits a value or emits a null value.

Of course, if you want the value in someObservable not to affect the creation of div, you can use ng-container as the parent of the text node.

Modify the code to use ng-container:

<div>
    <ng-container *ngIf="someObservable | async as someValue">
        {{ someValue.prop1 }} - {{ someValue.prop2 }}
    </ng-container>
</div>
Copy the code

Pits encountered by an Observable using multiple Async pipes

Here’s a problem with Jasmine testing an Observable with debounceTime and using async Pipe multiple times in the template

The Template code:

<div *ngIf="(book$|async)? .author && (book$|async)? .title">
  whatever..
</div>
Copy the code

Component code:

import { Component } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

@Component({
  selector: 'app-root'.templateUrl: './app.component.html'.styleUrls: ['./app.component.scss']})export class AppComponent {
  // Emulate the Observable mapping to the Template, note debounceTime!
  book$ = new BehaviorSubject({ title: 'titleTest'.author: 'authorTest' })
          .asObservable().pipe(debounceTime(200));
}

Copy the code

Jasmine unit test code:

import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent'.() = > {
  beforeEach(async() = > {await TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  });

  it('should render whatever', fakeAsync(() = > {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    // Simulate the lapse of 200 milliseconds
    tick(200);
    // Update the view manually
    fixture.detectChanges();
    /* The modified code will be added */
    const compiled = fixture.nativeElement;
    // I want the text displayed on the page to include 'whatever', where fail
    expect(compiled.querySelector('div').textContent).toContain('whatever');
  }));
});

Copy the code

After the command line allows ng serve, the page will normally display ‘whatever.. ‘

But when I run the ng test, Jasmine reported an error div was not created:

But when we add two more lines from the following comment (tick(200) + fixture. DetectChanges ()) to the test code, the test passes:

  it('should render whatever', fakeAsync(() = > {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    tick(200);
    / / first detectChanges computing (book $| async)? .author
    fixture.detectChanges();
    / /!!!!!! The following two behavior modification codes:
    // Simulate 200ms again
    tick(200);
    / / the second calculation (book $| async)? .title
    fixture.detectChanges();
    / /!!!!!! End of fix code
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('div').textContent).toContain('whatever');
  }));
Copy the code

This is because the template expression uses the am& & operator, which evaluates from left to right, meaning that the right-hand side of the operator is not evaluated until the left-hand side of the operator is true.

The first time the test ticks (200), because debounceTime(200) book$now receives the value {title: ‘titleTest’, author: ‘authorTest’}, after the first fixture. DetectChanges () success will be the first expression of the template (book $| async)? The author calculated is true; Second tick (200) + fixture. DetectChanges (), to bring the && right (book $| async)? The title value calculation is true, and successfully create a div.

Refactored template:

<ng-container *ngIf="(book$|async) as book">
  <div *ngIf="book.author && book.title">
    whatever.. 
  </div>
</ng-container>
Copy the code

Tests remain without additional tick,detectChanges versions, that is:

  it('should render whatever', fakeAsync(() = > {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    tick(200);
    / / first detectChanges computing (book $| async)? .author
    fixture.detectChanges();
    /* No longer needs the following code tick(200); / / the second calculation (book $| async)? .title fixture.detectChanges(); * /
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('div').textContent).toContain('whatever');
  }));
Copy the code