angular 2 change detection and ChangeDetectionStrategy.OnPush

*ngFor does it's own change detection. Every time change detection is run, NgFor gets its ngDoCheck() method called and there NgFor checks whether the content of the array has changed.

In your case there is no change, because the constructor is executed before Angular starts to render the view.
If you would for example add a button like

<button (click)="persons.push({name: 'dynamically added', id: persons.length})">add</button>

then a click would actually cause a change that ngFor has to recognize.

With ChangeDetectionStrategy.OnPush change detection in your component would be run because with OnPush change detection is run when

  • a bound event is received (click)
  • an @Input() was updated by change detection
  • | async pipe received an event
  • change detection was invoked "manually"

Okay, since this took me a whole evening to understand I made a resume to settle everything in my head and it might help future readers. So let's start by clearing some things up:

Changes come from events

A component might have fields. Those fields only change after some sort of event, and only after that.

We can define an event as a mouse click, ajax request, setTimeout...

Data flows from top to bottom

Angular data flow is a one way street. That means that data doesn't flow from children to parents. Only from parent to children for instance via the @Input tag. The only way to make a upper component aware of some change in a child is through an event. Which brings us to:

Event trigger change detection

When an event happens the angular framework check every component from top to bottom to see if they have changed. If any has changed, it updates the view accordingly.

Angular checks every components after an event has been fired. Say you have a click event on a component that is the component at the lowest level, meaning it has parents but no children. That click could trigger a change in a parent component via an event emitter, a service, etc.. Angular doesn't know if the parents will change or not. That is why Angular checks every components after an event has been fired by default.

To see if they have changed angular use the ChangeDetector class.

Change Detector

Every component has a change detector class attached to it. It is used to check if a component has changed state after some event and to see if the view should be updated. When an event happen (mouse click, etc) this change detection process happens for all the components -by default-.

For example if we have a ParentComponent:

@Component({
  selector: 'comp-parent',
  template:'<comp-child [name]="name"></comp-child>'
})
class ParentComponent{
  name:string;
} 

We will have a change detector attached to the ParentComponent that looks like this:

class ParentComponentChangeDetector{
    oldName:string; // saves the old state of the component.

    isChanged(newName){
      if(this.oldName !== newName)
          return true;
      else
          return false;
    }
}

Changing object properties

As you might have notice the isChanged method will return false if you change an object property. Indeed

let prop = {name:"cat"};
let oldProp = prop;
//change prop
prop.name = "dog";
oldProp === prop; //true

Since when an object property can change without returning true in the changeDetector isChanged(), angular will assume that every below component might have changed as well. So it will simply check for change detection in all components.

Example: here we have a component with a sub component. While the change detection will return false for the parent component, the view of the child should very well be updated.

@Component({
  selector: 'parent-comp',
  template: `
    <div class="orange" (click)="person.name='frank'">
      <sub-comp [person]="person"></sub-comp>
    </div>
  `
})
export class ParentComponent {
  person:Person = { name: "thierry" };     
}

// sub component
@Component({
  selector: 'sub-comp',
  template: `
    <div>
      {{person.name}}
    </div>
})
export class SubComponent{
  @Input("person") 
  person:Person;
}

That is why the default behavior is to check all components. Because even though a sub component cannot change if its input haven't changed, angular doesn't know for sure it's input haven't really changed. The object passed to it might be the same but it could have different properties.

OnPush strategy

When a component is marked with changeDetection: ChangeDetectionStrategy.OnPush, angular will assume that the input object did not change if the object reference did not change. Meaning that changing a property won't trigger change detection. Thus the view will be out of sync with the model.

Example

This example is cool because it shows this in action. You have a parent component that when clicked the input object name properties is changed. If you check the click() method inside the parent component you will notice it outputs the child component property in the console. That property has changed..But you can't see it visually. That's because the view hasn't been updated. Because of the OnPush strategy the change detection process didn't happen because the ref object didn't change.

Plnkr

@Component({
  selector: 'my-app',
  template: `
    <div class="orange" (click)="click()">
      <sub-comp [person]="person" #sub></sub-comp>
    </div>
  `
})
export class App {
  person:Person = { name: "thierry" };
  @ViewChild("sub") sub;
  
  click(){
    this.person.name = "Jean";
    console.log(this.sub.person);
  }
}

// sub component
@Component({
  selector: 'sub-comp',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>
      {{person.name}}
    </div>
  `
})
export class SubComponent{
  @Input("person") 
  person:Person;
}

export interface Person{
  name:string,
}

After the click the name is still thierry in the view but not in the component itself


An event fired inside a component will trigger change detection.

Here we come to what confused me in my original question. The component below is marked with the OnPush strategy, yet the view is updated when it changes..

Plnkr

@Component({
  selector: 'my-app',
  template: `
    <div class="orange" >
      <sub-comp ></sub-comp>
    </div>
  `,
  styles:[`
    .orange{ background:orange; width:250px; height:250px;}
  `]
})
export class App {
  person:Person = { name: "thierry" };      
  click(){
    this.person.name = "Jean";
    console.log(this.sub.person);
  }
  
}

// sub component
@Component({
  selector: 'sub-comp',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="grey" (click)="click()">
      {{person.name}}
    </div>
  `,
  styles:[`
    .grey{ background:#ccc; width:100px; height:100px;}
  `]
})
export class SubComponent{
  @Input()
  person:Person = { name:"jhon" };
  click(){
    this.person.name = "mich";
  }
}

So here we see that the object input hasn't changed reference and we are using strategy OnPush. Which might lead us to believe that it won't be updated. In fact it is updated.

As Gunter said in his answer, that is because, with the OnPush strategy the change detection happens for a component if:

  • a bound event is received (click) on the component itself.
  • an @Input() was updated (as in the ref obj changed)
  • | async pipe received an event
  • change detection was invoked "manually"

irregardless of the strategy.

Links

  • https://hackernoon.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
  • http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/
  • https://angular-2-training-book.rangle.io/handout/change-detection/change_detector_classes.html
  • https://www.youtube.com/watch?v=X0DLP_rktsc

To prevent Application.tick try to detach changeDetector:

constructor(private cd: ChangeDetectorRef) {

ngAfterViewInit() {
  this.cd.detach();
}

Plunker

Tags:

Angular

Zonejs