Daniel Patrick Daniel Patrick - 1 month ago 14
TypeScript Question

How can I prevent the Angular async pipe from making frequent server calls when no results come back?

I'm using the

async
pipe in an ngFor to watch an Observable. The Observable is created by a service that hits my server, and on load time when ngFor loop is enumerated, the service correctly makes a call to the server.

Now for the part I don't understand: when any results come back everything happens as expected. But if the server responds with say, a 404 and no results are available for enumeration, the
async
pipe causes the service to continue to fire requests, milliseconds apart. This is obviously no good. What's the design expectation here and how can I gracefully handle an Observable that returns an error while using an async pipe?

In the Component template:

<li *ngFor="let person of persons | async">{{person.id}}</li>


In the Component body:

get persons: Observable<Person[]> {
return this.personService.list();
}


In the Service:

list(): Observable<Person[]> {
if (this.persons && this.persons.length) {
return Observable.from([this.persons]);
} else {
return this.http.get('/person')
.map((response: Response) => response.json())
.map((data: Person[]) => {
this.persons = data;
return data;
})
.catch((error: any) => {
let errMsg = "some error..."
return Observable.throw(errMsg);
});
}
}
}

Answer

I had a very similar issue, and it was due to how Angular2 change detection works. There are multiple solutions that worked for me.

Save a reference to observable so that it doesn't change

Right now your when you call persons getter, it always returns a new instance of Observable (e.g. a different object), so Angular reevaluates the whole thing, and during the next change detection cycle, it does it once again, and then again, ... So, this is how it can be solved:

@Component({
    selector: 'my-comp',
    template: `{{ _persons | async }}`,
}) export class MyComponent implements OnInit {
    ngOnInit() {
        this._persons = this.personService.list();
    }
}

Change the ChangeDetectionStrategy to OnPush

You may want to tell Angular: "I know what I'm doing, I'll tell you myself when some change occurred":

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    selector: 'my-comp',
    template: `
      {{ persons | async }}
      <button (click)="reload">Reload</button>
    `,
}) export class MyComponent implements OnInit {
    constructor(private cd: ChangeDetectorRef) {}

    ngOnInit() {
        this.persons = this.personService.list();
    }

    reload() {
        this.persons = this.personService.list();
        // this tells Angular that a change occurred
        this.cd.markForCheck();
    }
}

Both of these solutions worked for me, and I decided to go with the second approach, since it's also an optimization

Comments