Do Angular applications become stable after a single change detection cycle?

Gara Mohamed
Angular In Depth
Published in
6 min readJul 1, 2018
Photo by Bryan Goff on Unsplash

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

Introduction

One of the most important changes in Angular compared to its predecessor AngularJS, is the change detection mechanism. The heart of AngularJS- the digest loop- has been completely reworked on Angular. Here is a great article of Max NgWizard K explaining what happened.

There is no more loop in Angular. The change detection is done in a single iteration. It begins at the root component and walks recursively through the children until reaching the leaf components. Each component in the tree is checked at most once. This process is started each time the microtasks queue becomes empty.

In his great talk Change Detection Reinvented, Victor Savkin says:

“The new system is designed in the way that it gets stable after a single pass. So, the number of digest iterations is fixed at one.”

If we deep dive into the Angular sources, we see that :

  • There is no more loop controlling change detection.
  • The TTL variable is removed.

However, in some cases a single change detection pass is not sufficient and doesn’t guarantee a stable and coherent application state. In this cases, Angular runs additional change detection cycles.

Let’s explain that in more depth by debugging some trivial Angular applications.

Example 1: A single change detection tick

Our application has a single component showing a counter controlled by two buttons. The first allows the increment of the counter’s value and the second allows its decrement.

The code is pretty simple. We just have to note that to track the running of change detections, we made a console.log() call in the ngDoCheck callback.

When, we click on the Increment button, the application go through two steps:

  • The first step, is the application phase. In this step, our increment method of the AppComponent is executed.
  • The second step, is the framework phase. In this step, Angular launches its change detection process. The given process goes from the root component to the bottom ones. Each component’s inputs, queries and DOM are updated. Also, the lifecycle hooks are triggered.

In this first scenario, as we see in the console, a single ApplicationRef.tick() is executed. The TTL is indeed equals to one.

Let’s now move on to a little more complex case.

Example 2: A single change detection tick is not sufficient

Our second application shows the counter’s value three times on the screen.

  • The first uses an input element with an NgModel directive binding the input value to the counter.
  • The second is bound to the component local state.
  • The third uses an exported NgModel directive.

The component class is the same as the first example. Only the template is modified.

Now, if we click on increment, we see two times the message tracking change detection run. So what’s happening? Doesn’t the Angular team said that we only need a single iteration to get a stable state?

Before jumping to the Angular team explanation, we will try to understand what is going on by placing a break point at the beginning of the ApplicationRef.tick() method. So, we can analyse the state of the view before each tick.

The first time we stopped at the break point, all the values are the same.

If we go ahead until the next break, we would see that the counter values are inconsistent. The exported directive value is incorrect and the others are correct. When the second change detection cycle is finished, the three values become the same and the application returns to a stable and consistent state.

This scenario shows us that a single change detection cycle is not sufficient. Let’s try to run through what happened within the two ticks to understand why that is.

In the first tick:

  • AppComponent is checked and {{ counter }} is updated with the new counter value. At this point, the NgModel directive is not yet checked, so {{ counterValue.value }} keep his old value.
  • Then, the NgModel directive is checked. The directive’s ngModel input changes wich triggers the update of the input element value. The value exported to the parent is also updated, but the parent component’s DOM will not reflect this change. For this reason the NgModel (in his ngOnChanges) triggers a second tick that will update the DOM of the parent component. The change detection cycle is triggered by scheduling a microtask (The ApplicationRef.tick() method run each time the microtasks queue becomes empty).

In the second tick:

  • AppComponent is checked and the obsolete expression {{ counterValue.value }} gets finally updated.
  • Then NgModel directive is checked. The directive is already up to date so there is nothing left to be updated.

In this scenario, the TTL is not really one but it’s two. The second tick is imposed by the exportAs feature. In this commit Tobias Bosch added an excellent comment that explains the problem related to exportAs directives property.

Now, let’s end up with our last scenario.

Example 3: Change detection hell loop

In the two first examples the change detection process is totally scheduled and controlled by the framework. But the framework user can make use of the different lifecycle hooks to execute some helpful code during the change detection phase. It may even add a code that would freeze the application. Let’s take a look on how would that happen on a modified version of our
first application:

In this new version, we used the ngDoCheck lifecycle hook to schedule a simple microtask that prints a sentence on the console. This microtask will be executed before all the eventually scheduled macrotasks.
After, it’s completed and if there is no more microtasks waiting on the dedicated queue, Angular will start a new change detection cycle. In this latter change detection cycle we will walk through the ngDoCheck a second time. A new microtask will be scheduled and the hell will continue undefinitely.

To avoid this hell loop, we added a TTL counter to stop the loop at the fifth change detection execution.

Note: If we schedule two microtasks in our ngDoCheck, the tick will be started after they both complete. That’s why Tobias Bosch said in the notes of his previously mentioned comment that: ‘this is just one extra run no matter how many `ngModel` have been changed’. Indeed, if there are many NgModel instances and if each of them schedule a microtask, the change detection will run just once after all the microtasks finishes.

Conclusion

The Angular change detection mechanism was reinvented to remove the digest loop and to force the one way data flow (from parents to children). But a developper can misuse lifecycle hooks methods and create an infinite loop: a change detection cycle trigger an other one and so on so forth.

After an event occurrence, the application should return to a consistent state in a single cycle or at worst in two cycles.

--

--