Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
            });
        }
    }
}
like image 709
Daniel Patrick Avatar asked Jul 25 '16 13:07

Daniel Patrick


People also ask

Does async pipe automatically unsubscribe?

Every time a new value is emitted the async pipe will automatically mark your component to be checked for changes. And best of all, when the component is destroyed the async pipe will automatically unsubscribe for you. No need to do this manually.

How do async pipe handle errors?

In order to handle errors properly, we are going to use *ngIf with async pipe. Even when using *ngFor , we need to wrap it around an *ngIf container. This is because *ngIf provides else statement, allowing us to provide a template if the operation fails or the request is still in progress.

Why is async pipe impure?

Because of the way Promise s work, Angular's async pipe has to be impure (meaning that it can return different outputs without any change in input). The transform method on Pipe s is synchronous, so when an async pipe gets a Promise , the pipe adds a callback to the Promise and returns null.

Does async pipe cache?

The pipe must cache the input and output to return the cached output value if the input matches the cached input value. If the input does not match the cached value, the pipe cannot return the output for the new input because it does not have it ready in time.


2 Answers

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

like image 89
Dethariel Avatar answered Oct 12 '22 18:10

Dethariel


Angular will check for changes when an even occurs. It means it will call the variables present in the template to see if their result changed or not.

If at each call you return a different object or array (even if the content is the same) angular will consider the content to have changed and refresh the template (resubscribing from the async pipe).

In your case you are calling "persons" which will return every time a different observable.

One solution is to affect the observable to a local variable and then to use this local variable in the template. Something like that:

class myComponent {
    public $persons: Observable<Person[]>;
    constructor() {
        this.$persons = this.$getPersons();
    }
    
    private $getPersons(): Observable<Person[]> {
        return this.personService.list();
    }
}

And your template

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

Another way is to use the decorator @Memoize which will cache the result of the method and return the same object once for all.

https://www.npmjs.com/package/typescript-memoize

Keep your code and just add the decorator it to your getter:

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

Bonus: You probably want to cache the last value received from your service (reason why you have the "if condition"). You should consider using a replay (publishLast or publishReplay)

like image 36
Flavien Volken Avatar answered Oct 12 '22 19:10

Flavien Volken