angular 2 change detection and ChangeDetectionStrategy.OnPush
Asked Answered
M

5

42

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 ?

Mandamandaean answered 30/9, 2016 at 16:1 Comment(3)
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.Anguilla
@Anguilla what do you mean ?Mandamandaean
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.Anguilla
M
112

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

Mandamandaean answered 1/10, 2016 at 2:35 Comment(3)
good answer, consider adding this article to the list. it goes long way explaining mechanics of change detection under the hoodSwordsman
this is a superb answer.Commonweal
thanks, this is good answer, this clear me out about how changeDetection work, Angular 2 takes care of firing the detect change process as soon as event occur, we - developer - mostly just care of whether the components should be updated!Luminal
A
23

*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"
Anastos answered 30/9, 2016 at 16:3 Comment(11)
You can't change change detection strategy for *ngFor. What you can do is to control when your code adds data to the array *ngFor iterates over.Yawata
I'm confused by your answer. You say there is no change in my case, but there IS a change when you click on the names. I guess the second part of your answer actually explains why that is.. I think you misunderstood my example, what I was reffering to is the fact that h2 changes its content on click. You said that change detection is run, yes, but it shouldn't notice a change, since the ref of the obj hasn't changed.Mandamandaean
Oh, sorry. I missed that click handler. But as I mentioned, a bound event causes change detection to be run on that component.Yawata
Change detection doesn't recognize the change. *ngFor recognizes the change.Yawata
See also github.com/angular/angular/blob/… ngDoCheck is called every time change detection is run. (click) causes change detection to run.Yawata
if(changes) checks if the IterableDiffers, executed previously, found any changes. No worries, change detection is quite hard to grasp. I don't have deep knowledge yet myself. You can check the @Directive() decorator. There is no change detection strategy defined, hence it uses default.Yawata
Head's up: Because someone was confused with your answer I switched the accepted answer to mine. I reread your answer and I think the first half is quite confusing (and you actually misunderstood the question at first). I don't usually accept my answers so I felt like telling you.Mandamandaean
Confused about what exactly?Yawata
You state that there is no change and my question was about a change. I think the answer is therefor misleading.Mandamandaean
"bound event is received" does it mean native event handlers (click) or Angular events (@Output)?Krill
@Krill Both, every event bound to using (foo)="..." or by @HostListener()Yawata
I
7

To prevent Application.tick try to detach changeDetector:

constructor(private cd: ChangeDetectorRef) {

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

Plunker

Iterative answered 30/9, 2016 at 16:6 Comment(0)
F
2

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

Formative answered 18/7, 2018 at 4:18 Comment(0)
I
0

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.

Imogene answered 18/4, 2020 at 21:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.