Change Detection in Angular 2

Understanding Zones and Change Detection in Ionic 2 & Angular 2



·

Zones and change detection are an important part of Angular 2, and so it is important to our Ionic 2 application. I figured I understood what was happening at a surface level with change detection, but didn’t really “get it”. I decided to do a deep dive into some research to try and clarify my understanding, and this article is my attempt at summarising what I discovered by reading lots of articles by people smarter than me.

This article aims to provide a simple introduction to Zones, and how they are important to change detection in Angular 2 and Ionic 2.

What is Change Detection?

Pascal Precht describes the task of change detection eloquently here:

The basic task of change detection is to take the internal state of a program and make it somehow visible to the user interface.

Another name for change detection is “dirty checking”. Compared to Angular 1, change detection in Angular 2 seems like magic. Generally, something changes and Angular 2 just somehow knows about it and updates the view accordingly. Of course, it isn’t magic, there’s a perfectly logical explanation that we are going to explore in this article. The TL;DR version I was able to come up with of how change detection in Angular 2 works is this:

Angular 2 runs inside of its own special zone called NgZone. Running inside a zone allows one to detect when asynchronous tasks – things that can alter the internal state of an application, and therefore its views – start and finish. Since these asynchronous tasks are the only thing that are going to cause our views to change, by detecting when they are executed Angular 2 knows that a view may need to be updated.

We can go into a lot more detail than that though, and I think understanding those details to some degree helps solidify the concept. So let’s continue.

What are Zones?

I mentioned above that Angular 2 implements its own special zone called NgZone. This special zone extends the basic functionality of a zone to facilitate change detection. But what the heck is a zone anyway?

A zone is not a concept that is specific to Angular 2. Zones can be added to any Javascript application with the inclusion of the Zone.js library, and the project itself describes zones as:

…an execution context that persists across async tasks. You can think of it as thread-local storage for JavaScript VMs.

I think this is a good analogy, but there are a few concepts mentioned in that definition which, depending on your level of knowledge of Javascript and programming in general, you may or may not understand. So let’s expand on that a bit.

First of all, let’s get the easy ones out of the way. An asynchronous task is a task that runs outside of the normal flow of the program – the program will go on executing without waiting for the asynchronous tasks to finish. When the async task does finish the application can handle it through the use of something like a callback or a promise. All asynchronous tasks in Javascript are added to an event queue, and tasks in the event queue are executed by the event loop when there is time (i.e. when the stack is empty, the event loop will push the task onto the stack):

Javascript Event Loop Diagram

The image above gives a visualisation of this. Normal (synchronous) function calls in a program are added to the “stack”, which are then executed from the top down. Any asynchronous function calls are added to the “event queue”. Once the stack is empty, the tasks in the queue can begin being processed.

A JavaScript Virtual Machine (VM), or a JavaScript engine, is a program that executes JavaScript code. Different browsers build different engines that interpret and run JavaScript code differently, like Google’s V8 engine and Apple’s Nitro. To us, it’s mostly the same, but different engines will have (slightly) different interpretations of syntax and performance.

Thread-local storage is a programming concept that essentially creates global memory for a specific thread. A thread is a bit of code that can run independently of other bits of code that may be running at the same time. So a “thread” is somewhat similar to an asynchronous task in Javascript, in that it doesn’t run in line with other tasks. This is important for an operating system (and other things) because it allows the computer to multi-task. You probably have 5 or more applications open on your computer right now, running a bunch of different processes. Threading allows the sharing of processing power on your computer, switching back and forth between different threads as required, creating the illusion that your computer is doing multiple things at once. Really, your computer is still only doing one thing at a time (if you have a single processor), but it is actually doing very, very tiny single bits of work at a time through the use of threads.

So since different threads – which execute some logic – run at effectively the same time, it creates the perfect environment for conflicts and race conditions. If a thread wanted to store some global variable, and another thread accesses the same variable, the end result of the program may depend on the times at which each thread executed – which is unpredictable and unrepeatable. Thread-local storage allows a thread to have its own global space to store variables, independent of another thread.

Putting those definitions together, we could change that original analogy to say:

You can think of zones as a way for the Javascript engine to create spaces to run code independently of other bits of code.

So why are zones – these independent execution contexts – important to change detection? The important part is that the zones “persist across async tasks”, so an asynchronous task will run within the same zone that it was created. Zones don’t just allow us to run bits of code in their own happy little worlds, they also allow us to hook into them to detect when asynchronous tasks start and finish.

I think Pascal Precht’s article on Understanding Zones does a great job at explaining this, so I hope he doesn’t mind me borrowing his example here. If you consider the following code:

function doSomething() {
  console.log('Async task');
}

// start timer
start = timer();
foo();
setTimeout(doSomething, 2000);
bar();
baz();
// stop timer
time = timer() - start;

We execute a series of functions, and we have a timer set up to detect how long it takes to execute. The issue in the code above though is that we use a setTimeout which creates an asynchronous task that will be added to the event queue. The code doesn’t wait around for the 2 seconds it takes for that task to finish, instead it will go on executing bar() and then baz() and then it will stop the timer – all before the doSomething task has had a chance to run. So the execution time that will be reported won’t really be accurate (if we want to consider the 2 seconds that it takes for doSomething to trigger).

Imagine this is our Ionic 2 application – if we set up our view once the code above finishes executing, how is it supposed to know that something may have changed once doSomething actually does finish, unless we were to manually make some call from doSomething to notify our application that a change is required.

Now take a look at another example from the same article:

var myZoneSpec = {
  beforeTask: function () {
    console.log('Before task');
  },
  afterTask: function () {
    console.log('After task');
  }
};

var myZone = zone.fork(myZoneSpec);
myZone.run(main);

We are creating a new zone by forking the parent zone and providing it with a “spec”. This spec uses the beforeTask and afterTask hooks, which can detect when an asynchronous tasks starts and finishes in a zone. We could then use those hooks to accurately time how long all of the code takes to complete, including the asynchronous doSomething function, because doSomething will trigger these hooks. Once we have our special zone created, we run the same program inside of myZone.run. This is essentially what Angular 2 does to create its custom NgZone zone and set up change detection (which we will talk more about in just a moment).

So, any time that a task is executed within the context of a zone, we can detect when it starts and when it finishes. This is very powerful.

How Does Change Detection Work?

By now, you probably have a pretty decent idea of how change detection works. Angular 2 has its own zone – its own execution context – and it can detect when any asynchronous task starts or finishes within that zone. So any task that is executed within Angular 2’s zone will trigger a change. That’s an important concept to understand, because anything executed outside of Angular 2’s zone won’t trigger a change.

We’re going to go into a little bit more depth on how this works, but still keep it pretty surface level. If you want a very low-level explanation of how change detection works in Angular 2, I would recommend reading Angular 2 Change Detection Explained.

When we first run our application our code will start executing. The root component is created and bootstrapped, our components are created, all of our constructor functions in our components will execute their code and so on. Eventually, once everything settles down (the stack is empty), our application reaches a nice resting state. Everything has been determined, and our views can reflect that.

An application that doesn’t change is pretty boring though, in fact, one could argue it’s not an application at all! So once that initial state of the application is determined, there are a few ways that the state can change – all of which are caused by asynchronous tasks, which can be:

  • Events like (click)
  • Http Requests like http.post
  • Timers like setTimeout

A user clicking a button can happen at any time, and that button click may cause a change to a view. A HTTP request could be triggered immediately by our application, or it could be triggered by a user clicking on a button, either way it could take anywhere from 10 milliseconds to 10 seconds (to never) to complete, and it could also cause a change to a view.

Let’s take a look at the following example:

private myTitle: string = "Hello";

constructor(){
    setTimeout(() => {
        this.myTitle = "Goodbye!";
    }, 5000);
}
<ion-title>{{myTitle}}</ion-title>

In this example we initially set the member variable myTitle to “Hello”. So once our application has finished loading and reaches that first “resting state”, myTitle will be “Hello” and the template will know to display it as the <ion-title>.

We have a timer here though, and after 5 seconds it is going to alter the state of the application by changing myTitle to “Goodbye!”. The problem is: how is Angular supposed to know about this? Well, we already know it uses zones to figure this out, but how?

As I mentioned, NgZone is a special type of zone created by Angular 2. It forks its parent zone, which allows it to set up its own functionality on that zone. This extra functionality includes adding an onTurnDone event (similar to afterTask that we discussed before), which triggers when Angular’s zone finishes processing the current turn – a turn is what happens when the event loop pushes a task from the event queue (an asynchronous task) to the stack.

If we take a look at the following code example from Zones in Angular 2:

ObservableWrapper.subscribe(this.zone.onTurnDone, () => {
  this.zone.run(() => {
    this.tick();
  });
});

tick() {
  // perform change detection
  this.changeDetectorRefs.forEach((detector) => {
    detector.detectChanges();
  });
}

we can see that every time a “turn” completes in Angular’s zone it triggers the tick function. If there were some asynchronous tasks, once the stack is emptied they would be added to the stack and executed – once those have finished executing Angular would be notified that another turn has completed. This tick function then triggers change detection for each component every time a “turn” completes. Each component in Angular 2 has its own change detection, so the entire component tree doesn’t need to be updated for every change. In effect, every time an asynchronous function finishes executing, Angular triggers tick which checks for changes.

The main thing to remember is that in order for something to trigger change detection it needs to be executed within Angular’s zone. If you’ve been developing Ionic 2 applications for a little while now you may have even used NgZone directly like this:

this.zone.run(() => { 
    // some code
});

this forces whatever is inside of the handler for the run method to run inside of Angular 2’s zone, which will ensure that it triggers change detection. Similarly, we could also run something outside or Angular 2’s zone:

this.zone.runOutsideAngular(() => {

  // do something

  // reenter Angular zone
  this.zone.run(() => {
    // back in Angular town
  });
});

This would instead run the code in Angular 2’s parent zone, the zone that Angular 2’s zone is forked from, so that it won’t trigger change detection.

Summary

There’s plenty more to know about change detection in Angular 2, and if you want to dive even deeper I’ll link some resources below for you to take a look at. This article should give you a broad enough understanding to be comfortable with change detection and zones though. In reality, you’re probably not going to need to implement anything we’ve discussed here yourself, but if you are having trouble with change detection it will be handy to be able to try and figure out why it is happening.

Further Reading

What to watch next...

  • Pingback: High Performance List Filtering in Ionic 2 | joshmorony - Build Mobile Apps with HTML5()

  • Pingback: Adding Background Geolocation to an Ionic 2 Application | joshmorony - Build Mobile Apps with HTML5()

  • Great article, it explains how angular 2 change detection works, but when I as a developer need to use zone in my application, I mean what is the use-case in app development?

    • Yasir Panjsheri

      any updates on this, i am also wondering about the use-case in app development

      • Right now, I don’t use it in normal flow, and when changing model doesn’t update UI, I start using it.

      • Yasir Panjsheri

        Hi, can you give a simple example of when UI does not update! (do you mean when we have an async task retrieving data but no being updated to .html)

  • Fernando Cebollada Aucejo

    After close the article and try to put on practice the knowledge learned here. I needed find again this article just to say that i can understand the issue that i have and i can fixed.

    thanks for this

  • Ncifra

    Hello Josh,

    After searching some up and down, it seems I have a problem with zones, but that I’m not being able to resolve. This is mainly due to the noncompatibility of firebase and cordova on Ionic 2, and implementing the functionalities without AngularFire2. In the code below, I try to do a Facebook login to get the credentials, and then do a Firebase providers link (that is an existing Google Plus account gets linked to the current Facebook account). The ‘linkWithFB()” function just calls firebase’s link() method (firebase.auth().currentUser.link(credential)).

    The thing is that even though the boolean “hasFacebook” changes, it doesn’t propagate the change to the layout. I tried adding the zone after the Facebook login, but to no avail. It seems this puzzled the ones at stackoverflow too, since they didn’t have any right reply.

    Facebook.login(permissions)
    .then((response) => {
    let facebookCredential = firebase
    .auth
    .FacebookAuthProvider
    .credential(response.authResponse.accessToken);
    this.zone.run( () => {
    this.authData.linkWithFB(facebookCredential)
    .then((user) => {
    console.log(“Account linking success” + JSON.stringify(user));
    //TO DO – Check why this doesn’t update the layout of the HTML
    this.hasFacebook = true;
    //window.location.reload();
    }, function (error) {
    console.log(“Account linking error”, error);
    })
    });
    }, function (error) {
    console.log(“Account linking error”, error);
    });

    Thanks

    • Question, is there a particular reason you are not allowed on using angularfire2 for this ?