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