Angular Change Detection is not triggered with nested ngFor

Viewed 96

I am currently working on a larger Angular project with a NgRx Store. The app contains a infinite scrolling list that when reaching the end shows a few skeleton items that when the request is finished get pushed down by real items.

In one component we need a grouped/nested list which results in a nested *ngFor. Since this is recomended for working with NgRx the component uses ChangeDetectionStrategy.OnPush.

Now when loading the list values from the backend the list takes a very long time to update (and at times only updates when clicking inside the browser) even though the request has been already finished for a long time. My first guess would be that there is a problem with the change detection.

This is an excerpt of what my component looks like at the moment:

<ul cdkScrollable #groupList>
  <li *ngFor="let dayGroup of groupsWithSkeleton$ | ngrxPush | keyvalue: asIsOrder;
              let i = index;
              trackBy: trackByKey">
    <div>{{ dayGroup.key | date }}</div>
    <ul>
      <li *ngFor="let subGroup of dayGroup.value | keyvalue: asIsOrder;
                  let i = index;
                  trackBy: trackByKey">
        <!-- stuff -->
      </li>
    </ul>
  </li>
</ul>
private groups$: Observable<Groups> = this.store.select(
  Selectors.selectGroups
);

constructor(readonly store: Store<AppState>) {
  this.groupsWithSkeleton$ = combineLatest([
    this.groups$,
    this.isLoadingOrHasMoreItemsAvailable$
  ]).pipe(
    map(([groups, isLoadingOrHasMoreItemsAvailable]) => {
      const skeleton: Groups = isLoadingOrHasMoreItemsAvailable ? this.skeletonMap : new Map();
      return new Map([...groups, ...skeleton]);
    })
  );
  // [...] more assignments
}

I already kind of found a working solution, but I think it is rather ugly and I don't realy understand why this does not work in the first place. (It was still not perfectly fast but acceptable.)

The solution was to add this after the creation of the group with skeleton items:

tap(() => {
  changeDetectorRef.markForCheck();
  changeDetectorRef.detectChanges();
})

I also created a StackBlitz with a MWE: https://stackblitz.com/edit/angular-ivy-7ycaxv?file=src/app/app.component.ts

Maybe someone can help me to find the underlying problem and a better solution.

1 Answers

That's a tricky one where the documentation is really not explicit about it :

We can find in the code source of the scroll dispatcher about scrolled :

In order to avoid hitting change detection for every scroll event, all of the events emitted from this stream will be run outside the Angular zone. If you need to update any data bindings as a result of a scroll event, you have * to run the callback using `NgZone.run

That's why you need to either explicitly call something that will trigger CD. In your case, changeDetectorRef.detectChanges() is enough !

Related