Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular async pipe not triggering change detection from NgOnChange

I came across such code. The problem was that progress bar was not disappearing after selecting item that already was in cache (when api call inside cache was made it works fine). What I was able to came up with was that change detection was not being run after executing operation in tap. Can someone explain to me why ?

@Component({
    selector: 'app-item',

    templateUrl: `
       <app-progress-bar
          [isDisplayed]="isProgressBar"
       ></app-progress-bar> 
       <app-item-content
          [item]="item$ | async"
       ></app-item-content>`,

    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemComponent {
    @Input()
    set currentItemId(currentItemId: string) {
        if (currentItemId) {
            this.isProgressBar = true;
            this.item$ = this.cache.get(currentItemId).pipe(
                tap(() => {
                    this.isProgressBar = false;
                    //adding this.changeDetector.detectChanges(); here makes it work
                })
            );
        } else {
            this.isProgressBar = false;
            this.item$ = of(null);
        }
    }
    item$: Observable<ItemDto>;
    isProgressBar = false;

    constructor(
        private cache: Cache,
        private changeDetector: ChangeDetectorRef
    ) {}
}

Cache is storing items in private _data: Map<string, BehaviorSubject<ItemDto>>; and is filtering out initial null emit

also changing

<app-progress-bar
   [isDisplayed]="isProgressBar"
></app-progress-bar> 

to

<app-progress-bar
   *ngIf="isProgressBar"
></app-progress-bar> 

makes it work without manually triggering change detection, why ?

Cache:

export class Cache {
private data: Map<string, BehaviorSubject<ItemDto>>;

get(id: string): Observable<ItemDto> {
   if (!this.data.has(id)) {
      this.data.set(id, new BehaviorSubject<ItemDto>(null));
   }
   return this.data.get(id).asObservable().pipe(
      tap(d => {
         if (!d) {
            this.load(id);
         }
      }),
      filter(v => v !== null)
   );
}


private load(id: string) {
   this.api.get(id).take(1).subscribe(d => this.data.get(id).next(d));
}

Edit:

So I figured: tap is being run as an async operation that is why it is being executed after change detection has already run on component. Something like this:

  1. this.isProgressBar = true;
  2. change detection run
  3. tap(this.isProgressBar = false;)

But I was fiddling with it and made something like this:

templateUrl: `
           <app-progress-bar
              [isDisplayed]="isProgressBar$ | async"
           ></app-progress-bar> 
           <app-item-content
              [item]="item$ | async"
           ></app-item-content>`,

        changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ItemComponent {
        @Input()
        set currentItemId(currentItemId: string) {
            if (currentItemId) {
                this.itemService.setProgressBar(true);
                this.item$ = this.cache.get(currentItemId).pipe(
                    tap(() => {
                        this.itemService.setProgressBar(false);
                    })
                );
            } else {
                this.itemService.setProgressBar(false);
                this.item$ = of(null);
            }
        }
        item$: Observable<ItemDto>;
        isProgressBar$ = this.itemService.isProgressBar$;

And now I have no clue why after doing operation in tap() change detection is not being run on component, does it have something to do with zones ?

ItemService:

private progressBar = new Subject<boolean>();

    setProgressBar(value: boolean) {
        this.progressBar.next(value);
    }

    get isProgressBar$() {
        return this.progressBar.asObservable();
    }
like image 303
pstr Avatar asked Sep 20 '25 01:09

pstr


1 Answers

For me there's two main issues with your code :

  • Your cache probably doesn't emit new values (which I don't know because you didn't provide the implementation of it), meaning the async pipe doesn't get triggered,

  • because you detect onPush, your view is not being refreshed by anything : only the methods/properties you touch are being updated. item$ not being related to your progress bar, you don't see it being updated unles you use detectChanges (which triggers a component change detection).


Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!