Understanding Change Detection Strategy in Angular

Angular performs change detection on all components (from top to bottom) every time something changes in your app from something like a user event or data received from a network request. Change detection is very performant, but as an app gets more complex and the amount of components grows, change detection will have to perform more and more work. There’s a way to circumvent that however and set the change detection strategy to OnPush on specific components. Doing this will instruct Angular to run change detection on these components and their sub-tree only when new references are passed to them versus when data is simply mutated.

The following covers change detection for Angular 2+

Simple Example

It’s probably easier to explain change detection with a clear example, so let’s start with a component that looks like this:

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  foods = ['Bacon', 'Lettuce', 'Tomatoes'];

  addFood(food) {
    this.foods.push(food);
  }
}

And the template looks like this:

app.component.html

<input #newFood type="text" placeholder="Enter a new food">
<button (click)="addFood(newFood.value)">Add food</button>

<app-child [data]="foods"></app-child>

Our here’s our child component and template:

child.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html'
})
export class ChildComponent {
  @Input() data: string[];
}

child.component.html

<ul>
  <li *ngFor="let item of data">{{ item }}</li>
</ul>

Everything works as expected and new food items get added to the list, thanks to our input in the child component that receives its data from the parent. Now let’s set the change detection strategy in the child component to OnPush:

child.component.ts

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  @Input() data: string[];
}

With this, things don’t seem to work anymore. The new data still gets pushed into our foods array in the parent component, but Angular don’t see a new reference for the data input and therefore doesn’t run change detection on the component.

To make it work again, the trick is to pass a completely new reference to our data input. This can be done with something like this instead of Array.push in our parent component’s addFood method:

addFood(food) {
  this.foods = [...this.foods, food];
}

With this variation, we are not mutating the foods array anymore, but returning a completely new one. Et voilà, things are working again in our child component! Angular detected a new reference to data, so it ran it’s change detection on the child component.

This one area where using something like ngrx/store for state management can really become powerful, because most components can adopt an OnPush strategy, and ngrx will dispatch new references when data changes.

ChangeDetectorRef

When using a change detection strategy of OnPush, other than making sure to pass new references every time something should change, we can also make use of the ChangeDetectorRef for complete control.

ChangeDetectorRef.detectChanges()

We could for example keep mutating our data, and then have a button in our child component with a refresh button like this:

import { Component,
         Input,
         ChangeDetectionStrategy,
         ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  @Input() data: string[];

  constructor(private cd: ChangeDetectorRef) {}

  refresh() {
    this.cd.detectChanges();
  }
}

And now when we click the refresh button, Angular runs change detection on the component.

ChangeDetectorRef.markForCheck()

Let’s say your data input in actually an observable. Let’s demonstrate with example using a RxJS Behavior Subject:

app.component.ts

import { Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Component({ ... })
export class AppComponent {
  foods = new BehaviorSubject(['Bacon', 'Letuce', 'Tomatoes']);

  addFood(food) {
    this.foods.next(food);
  }
}

And we subscribe to it in the OnInit hook in our child component. We’ll add our food items to a foods array here:

child.component.ts

import { Component,
         Input,
         ChangeDetectionStrategy,
         ChangeDetectorRef,
         OnInit } from '@angular/core';

import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
  @Input() data: Observable<any>;
  foods: string[] = [];

  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit() {
    this.data.subscribe(food => {
      this.foods = [...this.foods, ...food];
    });
  }
}

This would normally work right out of the box the same as our initial example, but here new data mutates our data observable, so Angular doesn’t run change detection. The solution is to call ChangeDetectorRef’s markForCheck when we subscribe to our observable:

ngOnInit() {
  this.data.subscribe(food => {
    this.foods = [...this.foods, ...food];
    this.cd.markForCheck();
  });
}

markForCheck instructs Angular that this particular input should trigger change detection when mutated.

ChangeDetectorRef.detach() and ChangeDetectorRef.reattach()

Yet another powerful thing you can do with ChangeDectorRef is to completely detach and reattach change detection manually with the detach and reattach methods.

🚀 And there you have it! An easy way to tune your app's performance. If you want to dig deeper into your understanding of change detection, I recommend this very informative post by Pascal Precht of thoughtram.

✖ Clear

🕵 Search Results

🔎 Searching...