Change detection doesn't fire in setTimeout with ChangeDetectionStrategy.OnPush

Viewed 6300

I have two components, where I use ChangeDetectionStrategy.OnPush. A parent component:

    @Component({
     changeStaregy: ChangeDetectionStrategy.OnPush,
     template:`
        <button (click)="onClick()">clear</button>
        <div>
            <test [model]="model"></test>
        </div>`
    })
    export class AppComponent {
        model: TestModel;
        
        constructor(){
          this.model = { id: 1, text: 'bla bla bla'}
        }
    
        onClick() {
          this.model = new TestModel();
        }
    }

and a child component that just displays a data:

    @Component({
       changeStrategy: ChangeDetectionStrategy.OnPush,
       selector: 'test',
       template: `
        <div>
              <div> {{model.id}} </div>
               <div> {{model.text}} </div>
        </div>`
    })
    
    export class TestComponent {
       @Input() model: TestModel;
       
    }

When I click on the button "clear", it calls onClick() function, which assigns an empty entity to "model". This triggers a change detection, because the input was changed (OnPush strategy). But if I wrap assignment with an async call, the change detection doesn't work and therefore UI is not updated:

    onClick() {
      setTimeout(() => {
        this.model = new TestModel();
      }, 2000);
    }

Angular2+ has NgZone which patches a setTimeout function. The patched setTimeout must trigger the change detection, but in my case it doesn't. Why the change detection doen't work? How can I fix it?

4 Answers

Here are the steps as to what is happening in your scenario:

  1. The bound click event will mark the component and its ancestors as dirty when triggered. This happens before the onClick() method is executed.
  2. After the onClick() method executes, Angular calls ApplicationRef.tick() to kick off change detection. Because the ancestor components were marked as dirty, change detection happens on your OnPush components. Keep in mind that at this point, the setTimeout callback hasn't executed yet.
  3. After change detection completes, the components are no longer 'dirty'. The setTimeout callback then executes. After the setTimeout callback completes, ApplicationRef.tick() is called to kick off change detection again. However, because your AppComponent is marked with OnPush, and there is no @Input change (because AppComponent has no Inputs), and component and its ancestors aren't marked as dirty, Change detection won't go through to the child component. So the change to this.model won't be reflected in the DOM.

I found this helpful: https://www.mokkapps.de/blog/the-last-guide-for-angular-change-detection-you-will-ever-need

When using OnPush change detection strategy, change detection only runs when an input property changes, bound event is trigger from component or you manually detect changes.

In your example plunker the child component changes input by itself in changeSetTimeout and changeModelInZone.

When you click on the button second time bounded event is triggered due to which template is updated.

you can use a promise to keep Change Detection working.

  onDoSometing() {
   this.changeVariable = -1; // sample immediate change

   this.timeout(3000).then(v => {
    this.changeVariable = 2; //sample, put your code here
   });
  }



  timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
Related