Log In

Angular Change Detection


by Yariv Katz

In this lesson we will talk about change detection in angular. How does angular know that there is a change. When knowing there was a change how does angular reflect that change in our DOM. What is the OnPush strategy and how I can leverage that strategy to create better performance in my angular app.

What is Change Detection

I like to seperate the concept of Change Detection (CD) to 2 parts:
1 - knowing there is a change or there might be a change in our app, and that change should reflect on the DOM.
2 - Making the changes in our components and our DOM.
So if our component is in a certain state, that state might change due to something that happened, something changed in our component and it needs to change what is displayed in some way. So understanding change detection is understanding when it happens or what might trigger it, and also understanding how it reflects the changes.

When change detection happens

Angular will run change detection when it suspects a change might have happened (it doesn't mean that a change actually happened). So Angular will ask it self is it possible that something changed? if the answer is yes it will run CD. Let's go over what might trigger angular to suspect a change happened:
1 - Timers (setTimeout, setInterval)
2 - Events (button click, mouse over, etc.)
3 - Promises, Observables
4 - Ajax - server returns data.
So whenever angular sees that those events happen, angular will suspect a change in the components happened.

Zone.js

How can angular know when the change detection events happen? Angular uses a library called zone.js to be aware of the CD events. Zone.js is a library that adds execution context to our code. What does execution context actually means? Think of it kind of like placing all you code inside a closed room, where I can peek into that room and ask questions about the code in it. For example I can ask the room when a timer returns to be notified, or when a promise is resolved to be notified. Essentially this is what zone.js and execution context is doing, angular creates a zone called angular and asks the zone when timers are returnes or promise or events or server query returns, then angular will run change detection. There is a service called NgZone which you can inject that wraps angular zone.js. Common usage of the service is running code outside the angular zone, for example say you have a setInterval that checks something every 10 miliseconds and you don't want it to trigger change detection you can use this service to run the check outside of angular zone and this will not cause change detection.

render order

So angular using zone.js can tell when a change might happen. When angular redraws the component, and recalculates the changes in the template and update the DOM with those changes we refer to this action is render. So to understand how angular renders the component and in what order, we can place in the template a call to a function in the component that will print a message to the console. Let's start to experiment with angular change detection. Start a new angular project using the cli:

> npx @angular/cli new ng-cd-tutorial

We will create a pyramid of components, a root app component which will display 2 child components, and the child components each one will display 1 grandchild components.

> ng g c Child1
> ng g c Child2
> ng g c GrandChild1
> ng g c GrandChild2

Time to connect our pyramid of components. In the app component app.component.html we will connect child1 and child 2:

{{print()}}

<app-child1></app-child1>
<app-child2></app-child2>

The print method is used to print in the console that a render of the app component is being made. Let's create the print method in the app.component.ts:

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

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent {
    print = () => {
        console.log('rendering AppComponent');
    }
}

Similar print method we will add to the child1, child2, grandchild1, and grandchild2 where everyone will print the name of the component in the console. We are doing this cause we will know angular will run this method when it needs to render the component and it will help us determine the order that angular operates on the component tree. In the template of the Child1 we will place the grandchild1 component, modify the file child1.component.html

{{print()}}

<app-grand-child1></app-grand-child1>

Similar we will do to the child2 template only add the grandchild2 to it. Don't forget to place the print method in all the templates as well as the grabd children template. We created a nice pyramid that will help us understand the order of change detection. Now launch the app and let's examine what is printed:

rendering AppComponent
rendering Child1
rendering GrandChild1
rendering Child2
rendering GrandChild2

When angular renders components it always starts with the root, then child1 tree which includes grandchild1 then moves to the child2 tree which includes the grandchild2. Angular moves in a uni directional data flow from top to bottom. Let's try and trigger an action that cause change detection, like activating a timer. We will activate the timer in the Child2 to see what will rerender when a change in a certain component is triggered, will it rerender everything or just the component. Modify the child2.component.ts

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

@Component({
    selector: 'app-child2',
    templateUrl: './child2.component.html',
    styleUrls: ['./child2.component.scss']
})
export class Child2Component implements OnInit {
    ngOnInit() {
        setTimeout(() => {
        console.log('timer in child2');
        }, 1000)
    }
    
    print = () => {
        console.log('rendering Child2');
    }
}

After the timer is triggered we can see this result in the console

timer in child2
rendering AppComponent
rendering Child1
rendering GrandChild1
rendering Child2
rendering GrandChild2

So by default the order is always the same from top to bottom, no matter what component triggered the change.

Change Detection Strategy OnPush

Angular let's you change the default change detection strategy. You can set a component to a different strategy in the component metadata. We saw what happens on the default strategy every component from top to bottom will render. The other strategy you can choose is a strategy called OnPush. Before we examine when OnPush strategy will render let's change Child1 to that strategy and examine what happens. Modify the metadata in child1.component.ts

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

@Component({
    selector: 'app-child1',
    templateUrl: './child1.component.html',
    styleUrls: ['./child1.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class Child1Component {
    print = () => {
        console.log('rendering Child1');
    }
}

Now let's examine again, what happens when the timer in child2 activates. What is printed now is:

timer in child2
rendering AppComponent
rendering Child2
rendering GrandChild2

so the timer in child2 didn't trigger CD in the Child1 tree. In fact even if the timer was in Child1 or GrandChild1 it would still be the same result, the Child1 tree will not render and all the rest will. The child1 tree will not render but if we place a DoCheck hook, you will notice that the Child1 hook will be called (not the Grandchild1 do check hook). The DoCheck is another way we can cause an OnPush component to render by manually calling in that hook for the component to render. So timers will not cause the Child1 tree to render but let's see what happens with events. Let's place a button in the GrandChild1 and add a click event. grand-child1.component.html

<p>
    grand-child1 works!
</p>

<button (click)="clicked='you did it!'">Click me {{clicked}}</button>

{{print()}}

We added a button in the template with a click event that will place a string in a variable. Now when clicking the button you will notice that the Child1 tree is rendering. So this is printed:

rendering AppComponent
rendering Child1
rendering GrandChild1
rendering Child2
rendering GrandChild2

So an event in the OnPush tree will cause the tree to render. Note that even though the event was in the grandchild1 it will still render the child1. But if we had on push in the child1 as well as grand child1, and the event was in the child1n then the grand child1 will not render. Basically and event mark the components from top to the component that triggered the event as needs to render.

A promise, observable or server call will not cause render to our OnPush.

let's examine another things that will cause change detection in an OnPush component. The parent app component, will now transfer input to the child1. The input will change when we click a button in the app component. Modify app.component.html:

{{print()}}

<button (click)="obj={msg: 'hello'}">Click to change obj</button>

<app-child1 [inputObj]="obj"></app-child1>
<app-child2></app-child2>

we are now passing an input to child1 and we also added a button that will change that input when you click the button. We will notice that clicking the button will cause the Child1 on push tree to render. We are passing an object to the child, what will happen if we keep the same object only change the property inside. Change the app.component.html:

{{print()}}

<button (click)="obj['msg']='hello'">Click to change obj</button>

<app-child1 [inputObj]="obj"></app-child1>
<app-child2></app-child2>

Now if we click the button we are not changing the reference to the object so there will be no change detection in Child1 tree. So input properties change will also cause the components to render just as long as the entire reference change.

another way to cause change detection is by manually asking for it.

ChangeDetectorRef

The base class for angular views is the ChangeDetectorRef. A component in angular contains a class representing it's views. The ChangeDetectorRef job is to tell if the view is dirty and needs to render. In practice every component has a ChangeDetectorRef of it's own so every component has a service which determines if the view is dirty or not. So an angular app is made from a pyramid of ChangeDetectorRef. When a the ChangeDetectorRef determines the component as dirty it will mark the route from the top to the component as dirty and rendering will redraw the route from top to the component. When a component is in OnPush mode it will determine if the component is dirty based. So the process of rendering the component is simply go from top to bottom, if the component is in default then just continue down, if the component is in OnPush ask if the ChangeDetectorRef marked the component as dirty from the reasons we stated above. Let's try and inject the ChangeDetectorRef and create a timer (which usually won't cause render) and in the timer we will use the ChangeDetectorRef to force rendering. Modify the child1.component.ts

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

@Component({
    selector: 'app-child1',
    templateUrl: './child1.component.html',
    styleUrls: ['./child1.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class Child1Component {
    @Input() inputObj;

    constructor(private _cd: ChangeDetectorRef) {}

    print = () => {
        console.log('rendering Child1');
    }

    ngOnInit() {
        setTimeout(() => {
        console.log('This timer will cause rendering');
        this._cd.detectChanges();
        }, 2000)
    }

}

we injected the ChangeDetectorRef and in the the setTimeout in the OnInit hook we are calling the ChangeDetectorRef detectChanges() method which will call the child1 tree to render. Another method in that service is markForCheck which will cause render of the component but will start from the top. Meaning detectChanges will be a bit faster so it's better to use that if the parent components have no intrest.

async pipe

Another thing that will cause a render to OnPush component is by subscribing to Observables or Promises using the async pipe. Let's try to query our todo server and display the results. One way to do it will be: child1.component.ts

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

@Component({
    selector: 'app-child1',
    templateUrl: './child1.component.html',
    styleUrls: ['./child1.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class Child1Component {
    @Input() inputObj;

    tasks : any = []

    constructor(private _http: HttpClient) {}

    print = () => {
        console.log('rendering Child1');
    }

    ngOnInit() {
        this._http.get('https://nztodo.herokuapp.com/api/task/?format=json').subscribe((json) => {
            this.tasks = json;
        })
    }

}

Doing this will release the call to the server but will not cause the component to render since our component is in OnPush strategy and calling a server or subscribing to observables are not causing change detection in OnPush. One way we can solve this is to inject ChangeDetectorRef and manually call detectChanges. That will work but we rather not call it in every server call manually cause it could be tidious. We can use the async pipe to subscribe to observables in the template, observables / promises that we subscribe from the template will cause change detection on OnPush strategy. Change the child1.component.ts to this:

import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Component({
    selector: 'app-child1',
    templateUrl: './child1.component.html',
    styleUrls: ['./child1.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class Child1Component {
    @Input() inputObj;

    tasks$ : Observable<any>;

    constructor(private _http: HttpClient) {}

    print = () => {
        console.log('rendering Child1');
    }

    ngOnInit() {
        this.tasks$ = this._http.get('https://nztodo.herokuapp.com/api/task/?format=json');
    }

}

and to subscribe to that observable in the template child1.component.html

{{print()}}

<ul>
    <li *ngFor="let task of tasks$ | async">
        {{task.title}}
    </li>
</ul>

<app-grand-child1 [obj]="obj"></app-grand-child1>

Now our code has less nesting cause we are not subscribing in the code, plus or component will change detect when the server returns the data.

Summary

To summerize, to improve performance of our app we need to aim that the majority of our components will be set to OnPush strategy. This will cause the components to mark themselves as dirty in one of these cases:
1 - @Input properties change reference
2 - event that origin came from inside the OnPush tree
3 - Manually with the ChangeDetectorRef
4 - subscribing to observables or promises in the template with the async pipe.