angularsettimeoutangular2-changedetectionzonejsasync-pipe

Why does setTimeout inside runOutsideAngular callback skip change detection for the observable, even if markForCheck is called manually?


I've noticed a strange behavior of change detection in Angular. When Observable updated as in the example, change detection not triggered for some reason.

The key here is setTimeout called inside the callback, if you remove it, change detection will work fine. markForCheck which inside AsyncPipe also called as it should.

@Component({
  selector: 'my-app',
  template:
    '<button (click)="click()">Trigger</button> <br> {{value$ | async}}',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  readonly value$ = new BehaviorSubject(1);

  constructor(
    private readonly zone: NgZone,
  ) {}

  click() {
    this.zone.runOutsideAngular(() => {

      setTimeout(() => {
        this.value$.next(this.value$.value + 1);
        console.log(`Change (Should be ${this.value$.value})`);
      });

    });
  }
}

StackBlits example

StackBlitz with async pipe debug example


Solution

  • What happens:

    The click triggers a CD cycle and marks the OnPush component to be checked. Timeout is started. Component is checked for changes, but the value did not change yet and so the UI is not updated. CD cycle is done.

    Timeout elapses. It does not trigger another CD cycle, because it is inside runOutsideAngular. Value is nexted. Async pipe calls markForCheck, but this does not have any visible effect until another CD cycle is triggered, e.g. by clicking the button another time.

    If setTimeout is removed, the value is nexted in the same CD cycle that has been triggered by the button click, and the UI will be updated.

    If runOutsideAngular is removed, the elapsing timeout triggers a new CD cycle, the component will be checked for changes because the async pipe called markForCheck, and the UI will be updated.