Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

angular

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?

like image 436
Alexander Surkov Avatar asked Jul 24 '17 13:07

Alexander Surkov


3 Answers

Because the changeDetectionStrategy is set to OnPush on the parent, the change detection cycle stops at the parent element. As a result any child of this parent will have the strategy set to OnPush, regardless of its own setting.

No @Input of the parent changes, and therefor the change detector doesn't go in any deeper towards the childs of the parent. You should execute a detectChanges to have the changes take effect

like image 166
Poul Kruijt Avatar answered Oct 19 '22 18:10

Poul Kruijt


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

like image 4
Ahbert Avatar answered Oct 19 '22 18:10

Ahbert


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.

like image 1
alt255 Avatar answered Oct 19 '22 17:10

alt255