Serge Intern
Serge Intern

Reputation: 2969

ChangeDetectionStrategy.OnPush doesn't act how I expect it to

I'm trying to get familliar with angular 2's ChangeDetectionStrategy.OnPush performance boost (as explained here). But I have curios case here.

I have parent AppComponent:

@Component({
  selector: 'my-app',
  template: `<h1>
  <app-literals [title]="title" [dTitle]="dTitle"></app-literals>
  <input [value]="title.name"/>
</h1>
`
})
export class AppComponent implements OnInit {
  title = { name: 'original' };
  dTitle = { name: "original" };

  constructor(private changeDetectorRef : ChangeDetectorRef) {

  }

    ngOnInit(): void {
      setTimeout(() => {
        alert("About to change");
        this.title.name = "changed";
        this.dTitle = { name: "changed" };
      }, 1000);
    }

}

And child LiteralsComponent component:

@Component({
  selector: 'app-literals',
  template: `  {{title.name}}
  {{dTitle.name}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class LiteralsComponent implements OnInit {
  @Input('title') title;
  @Input('dTitle') dTitle;

  constructor() { }

  ngOnInit() {
  }

}

I thought setting strategy to OnPush made angular only reflect changes of references but in a sample I tried changing (mutate) a property of an object and angular still reflects it.

this.title.name = "changed"; shouldn't be detected (thus the UI shouldn't not reflect the change).

Here is the case on plunker

How comes? How to do it right?

Upvotes: 3

Views: 856

Answers (1)

Max Koretskyi
Max Koretskyi

Reputation: 105497

If I understand correctly you're asking why the bindings value is updated in LiteralsComponent template even if you don't modify the reference title, but rather mutate the object.

The short answer is that because you modify both:

this.title.name = "changed";
this.dTitle = {name: "changed"};

in the AppComponent.ngOnInit. If you modify only this.title.name = "changed" you will see that template is not updated.

However, this is a very interesting question to explore in details

Let's first start with only this.title without this.dTitle.
The first thing to understand is that when you specify the following in the template:

{{title.name}}

here is what Angular does. It tries to find title object on current component instance, and then gets name property from it and reflects it in the DOM. But with the following configuration:

class AppComponent {
    title = { name: 'original' }

    ngOnInit(): void {
      setTimeout(() => {
        alert("About to change");
       this.title.name = "changed";
    }, 1000);
}
}

class LiteralsComponent {
     @Input() title;
}

the title object is the same in both components (point to the same memory location).

So when Angular runs change detection for the LiteralsComponent component, it accesses the same object that you're changing here in AppComponent:

ngOnInit(): void {
  setTimeout(() => {
    alert("About to change");
    this.title.name = "changed";
  }, 1000);
}

The interesting observation here is the change isn't detected at all neither with OnPush nor without it:

class LiteralsComponent {
     @Input() title;

     ngOnChanges(changes) {
         // will be triggered only for the first CD cycle,
         // and won't be triggered when `title` is updated
     }
}

Now, the last thing is to understand is when the DOM is updated. According to this article, it's updated during CD for the current component. It means that if the current component isn't checked, the DOM won't be updated. So we specify onPush for the LiteralsComponent:

changeDetection: ChangeDetectionStrategy.OnPush,

the view won't be updated.

But, it's updated in your question. Why?

And this where dTitle comes into play. With this property, you're actually modifying the reference and Angular detects bindings changes and runs CD for the LiteralsComponent component. And we learnt above that when CD is run, the DOM is updated. So Angular also updates {{title.name}} since it points to the same object in AppComponent, although it didn't detect it was changed.

Upvotes: 8

Related Questions