Search code examples
angularsubscriberrxjs6

Angular: Observable subscribing in service - using it with ngOnChanges runs (too) many times


so I have a scenario like this (simplyfied)

  • main component
  • list component
  • list service

where:

main component

<my-list [month]="month"></my-list>

list component html

<li *ngFor="let item in list | async></li>

list component ts

list: Observable<ListItem[]>;
@Input() month: string;
...
ngOnInit() {
  this.list = this._listService.list;
}
ngOnChanges(changes: SimpleChanges) {
  if (changes.month && !!this.month) {
     this._listService.getAllByMonth(this.month);
  }
}

and finally list service

private _list: BehaviorSubject<ListItem[]>;
list: Observable<ListItem[]>;

private dataStore: {
  list: ListItem[]
};

constructor(private _http: HttpClient) {
  this.dataStore = { list: [] };
  this._list= <BehaviorSubject<ListItem[]>>new BehaviorSubject([]);
  this.list= this._list.asObservable();
}

public getAllByMonth(month: string) {
   this._http.get<ListItem[]>(environment.apiPath + 'list/' + month)
    .subscribe(
      data => {
        this.dataStore.list = data;
        this._list.next(Object.assign({}, this.dataStore).list);
      },
      error => console.log('Could not load the list.')
    );
}

For some reason getAllByMonth get's called many, many times... even though I change the month value only 1.

How should arrange things that getAllByMonth get's called ONCE when month value changes?


Solution

  • *ngFor uses the equals === comparison to see if an Array instance has changed. When the new array and old array are not the same reference, then all the elements in the collection are recreated.

    To tell *ngFor that it should not recreate an element, but re-use an element in the array. You have to use the trackBy callback function.

    <li *ngFor="let item in list | async; trackBy: trackItem"></li>
    

    Then in your component

    public trackItem(indx: number, item: LinkItem): number {
         return item.id;
    }
    

    This function needs to return a value (number, string, object, etc) that yields true for equal === comparisons. This is how ngFor knows that it should re-use a component with that array item.

    In my above example I assumed their is a unique number id that can be used from each LinkItem.

    All of this stops the recursive calls to ngOnChanges because those components were being recreated every time list was updated with a new array instance.