36
votes

I'm trying to understand the ChangeDetectionStrategy.OnPush mechanism.

What I gather from my readings is that a change detection works by comparing the old value to the new value. That comparison will returns false if the object reference hasn't changed.

However there seems to be certain scenarios where that "rule" is bypassed. Could you explain how does it all work ?

5
None of the answers actually show a good implementation of change detection strategy. But someone got it working? I need it for an issue of mine.Gary
@Gary what do you mean ?Ced
None of the plunkrs demonstrate changedetection strategy. They are being blocked from changes due to some other reasons or errors stoping changes since app broke.Gary

5 Answers

102
votes

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

22
votes

*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"
7
votes

To prevent Application.tick try to detach changeDetector:

constructor(private cd: ChangeDetectorRef) {

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

Plunker

1
votes

In angular we highly use Parent - child structure. There we pass Data form parent to child using @Inputs.

There, if a change occur on any ancestor of the child, change detection will happen down in the component tree form that ancestor.

But In most of situations we will need to update the view of the child (call Change Detection) only when it's inputs change. To achieve this we can use OnPush ChangeDetectionStrategy and change the inputs (using immutables) as required. LINK

0
votes

By default, everytime something changes(All browser events, XHR's, Promises, timers, intervals etc...) in an application, Angular runs Change detection for every component which is costly. When the application grows big, this may cause performance issues.

Change detection may not be needed for few Components for all kinds of changes mentioned above. So, by using onPush strategy, the change detection can be made to run on a particular component in the following scenarios

- The Input reference changes(Immutable inputs)
- An event originated from the component or one of its children
- Run change detection explicitly
- Use the async pipe in the view

Now, one may ask why Angular could not make onPush as default strategy. The answer is: Angular does not want to force you to use immutable inputs.