The code side of life

Angular 2 Change Detection, Zones and an example

Introduction

user

Shlomi Assaf


Angular Angular2 Change Detection Zones Zone.js NgZone

Angular 2 Change Detection, Zones and an example

Posted by Shlomi Assaf on .
Featured

Angular Angular2 Change Detection Zones Zone.js NgZone

Angular 2 Change Detection, Zones and an example

Posted by Shlomi Assaf on .

Handling DOM media query change event in angular 2, A real life scenario where Angular's change detection needs a little help.

If you have been playing around with angular you probably noticed that the infamous $digest / $apply is gone, no more loops!

In this post we will cover some of the concepts involved in Angular's change detection and touch a bit of Zones, at the end we will review an example where change detection will not work and how can we make it work.

The theory

In angular 2 change detection is deterministic which is just a fancy word for no surprises.
The change detection engine ("CDE") goes forward and never looks back starting from the top most component, A.K.A root component, going down to the last leaf component. This is also known as One-way data flow.

The CDE can do this because an Angular 2 application architecture is quite simple, a tree of components (leaving providers, pipes, etc aside for the moment).

Start from the root component AppCmp, each component can have 0...n child components:

Image taken from Victor Svakin's article: CHANGE DETECTION IN ANGULAR 2

As the drawing suggests, each components points to it's children but children does not point to a parent component. This one way data flow is important and what makes the no surprises thing possible and eliminates the need for the dreaded $digest loop.

The practice

Let's pause for a second, data flows in one direction? this is not applicable in real life, I don't want that!

Well, relax, data flows in all direction in Angular 2. One-way data flow is a concept, it describes:

  • The natural way for data flow (the one way).
  • Guidelines for data to flow in other directions.

Angular 2 brings the tools to implement this concept and bind's them together with the CDE creating a change detection schema for each component.

The most basic toolset

All of you'r angular component's will use the basic toolset, angular CDE is so powerful that its usually enough.

  • @Input - Describe a component property bound to the outside world - Data coming in.
  • @Output - Describe a component property sending messages to the outside world - Data going out.

It's quite clear the @Input is what we call one-way binding. Combining @Input & @Output together is two-way binding.
Look how simple, no more weird stuff ( <, @, = ), english!

More advanced toolset

Some scenarios require some heavy lifting, angular is flexible and let us the developers control the CDE, let go over them briefly.

  • ChangeDetectorRef - Each component has a unique CD instance to manually control change detection, ChangeDetectorRef is injectable.
  • NgZone - The power behind change detection, enables stepping out/in of CD marshaling.
  • ChangeDetectionStrategy - Instruction for the CDE how to identify a change.
  • Life cycle hooks - Enables hooking into points where data changes or a CD check is needed.
  • Observable objects - Combine with ChangeDetectionStrategy and you can say goodbye to change detection, here comes change notifications - get reactive!

As you can see there are a lot of tools for us developers, angular 2 doesn't provide a change detection engine but a change detection framework. Out of the box it will work blazing fast but if we want more we can opt in and squeeze the juice out of it.

More info

This is just the tip of the iceberg, while we did cover most of the tools available, it was a high-level overview. If you want to get deep I suggest reading ANGULAR 2 CHANGE DETECTION EXPLAINED by Pascal Precht and then dive into Victor Svakin's article CHANGE DETECTION IN ANGULAR 2

The example

Ok, so we're here for an example right, let's make it interesting.

I've already mentioned NgZone which is the angular implementation for the Zones concept and Zone.js library. If you don't know Zones don't panic, you can develop an angular application with zero Zones knowladge but I believe it's super important and it's worth spending the time on it as it will save you a lot of time later.

Zones

Zones requires a separate article but I will try to apply some context. A Zone is a context execution unit, which is a piece of code sharing some context. Each zone can fork and create a child zone with a different context, no limits.
Think javascript closures where a function has access to context declared in the blocks surrounding the function, this is zones but in a more controlled and organized way.
This is the magic in angular and how we got rid of $digest, NgZone wraps all possible ways a change might occur and fire's a change detection when one is needed.
The "wrapped ways" are actually asynchronous operations, mainly Events, XHR(http) and timers... that's it. Zones monkey patch these API's and controls their flow.

Angular run's in it's own forked zone, this means that events invoked within the zone will trigger a change detection scan, luckily its by default and we can also request to step out of the zone and run some code the will never trigger change detection, no matter what.

You can read more about Zones in ZONES IN ANGULAR 2 by Pascal Precht

In this example I will demonstrate a situation where change detection will not work, we will see why and how to by pass it. The most important thing is to understand why change detection did not work, if you got this consider yourself an angular change detection pro.

The scenario is simple, listen to DOM media query change events and update a property in a component, quite simple.

@Component({
  selector: 'my-app',
  styles: [`
    .box { width: 300px; height: 300px; }
  `],
  template : `<div class="box" [style.backgroundColor]="color"></div>
  `,
  directives : []
})
export class App {  
  public color: string = 'black';

  constructor() {
    const mql: MediaQueryList = window.matchMedia('(min-width: 600px)');

    mql.addListener((mql: MediaQueryList) => { 
        this.color = mql.matches ? 'blue' : 'yellow';
    });
  }
}

I will argue that property changes within the listener will not propagate, the color will not change. why? NgZone does not wrap the matchMedia API so it doesn't know that a change detection scan is needed.
If instead of registering a listen for MediaQueryList we would have registered it for a click event on the document the box color would change, since NgZone wraps all click event.

To make this work we need to run the change detection scan manually, there are several ways to do it:

  1. Inject ApplicationRef and call ApplicationRef.tick()
  2. Inject ChangeDetectionRef and run the scan directly.
  3. Run the listener inside an angular zone.

Option 1 & 2 will work just fine but they require an action, they are not part of the flow.
Option 3 is more natural, it does not require an action, let's implement it:

@Component({
  selector: 'my-app',
  styles: [`
    .box { width: 300px; height: 300px; }
  `],
  template : `<div class="box" [style.backgroundColor]="color"></div>
  `,
  directives : []
})
export class App {  
  public color: string = 'black';

  constructor(zone: NgZone) {
    const mql: MediaQueryList = window.matchMedia('(min-width: 600px)');

    mql.addListener((mql: MediaQueryList) => { 
      zone.run( () => { // Change the property within the zone, CD will run after
        this.color = mql.matches ? 'blue' : 'yellow';
      });
    });
  }
}

I've put together a simple demo in a plunker:

See the plunker

Thanks for reading, if you have any comments or suggestions please post below.

user

Shlomi Assaf