Understanding Angular's Change Detection

Angular has a built-in change detection mechanism, which ensures that component views get updated automatically when the underlying data changes. However, this behaviour is not always intuitive, and can be very frustrating to debug. This article seeks to give a brief overview of how this mechanism works, and explain some of its quirks.

When does change detection run?

Change detection runs in response to various asynchronous browser events, such as:

  • User input (mouse clicks, key presses, etc.)
  • Timers
  • Ajax requests

How does it work?

All JavaScript code within a webpage has an entry point. It may be executed when the page first loads, or perhaps in response to user input, or after a timer expires. But there is always some trigger that causes code to execute.

Angular hooks into these entry points when it first loads by overriding several built-in functions, for example, addEventListener, which is used to subscribe to browser events such as mouse clicks. Whenever a registered event listener gets called, Angular ensures that its own callback gets executed immediately afterwards, so that it can run change detection and, if necessary, update the view.

This is explained in more detail over at Angular University.

Change detection is not immediate

The result of this implementation is that change detection is always scheduled to run after our application's code finishes - not immediately after a value changes. This may seem obvious, but it can produce behaviour that may seem strange at first.

Take, for example, a component that takes a colour as an input, and has some means of communicating to its parent that the colour has been changed:

export class ColouredBlockComponent {

  @Input() colour: string;
  @Output() changeColour: EventEmitter<string> = new EventEmitter<string>();

  ngOnChanges(changes: SimpleChanges) {
      console.log('this.colour is: ' + this.colour);
  }

  itsRainbowTime() {
      changeColour.emit('red');
      changeColour.emit('orange');
      changeColour.emit('yellow');
      changeColour.emit('green');
      changeColour.emit('blue');
  }

}

One might expect the following output after calling itsRainbowTime():

this.colour is: red
this.colour is: orange
this.colour is: yellow
this.colour is: green
this.colour is: blue

But of course, Angular change detection only runs after the application finishes executing whatever callback it's in, at which point only the latest value can be seen, resulting in the output:

this.colour is: blue

Change detection is not called outside the Angular Zone

Another situation in which change detection may not run as expected is if the entry point of some code exists outside the Angular Zone. This may be the case for classes instantiated outside of the Angular context, for example, third-party libraries; callbacks from such libraries will not trigger change detection.

NgZone.isInAngularZone() can be used to test this, and ngZone.run() can be used to ensure code is executed inside the Angular Zone.

What counts as a change?

Knowing when change detection is running is one thing, but how does Angular actually recognise a change?

That depends on the ChangeDetectionStrategy. There are two possibilities here:

Default

Angular will examine all expressions within a component's template to see if any of their values have changed.

Note that when comparing values, Angular does not perform deep comparisons of objects.

To force Angular to recognise a change when modifying a property of an object, a new object should be created with the modified property instead. In other words, objects should be treated as immutable.

For example, if some object is defined:

let obj = { count: 0 };

Angular will not consider the object to be changed if a property is modified directly:

obj.count++;

Instead, a new reference should be created (shown here using the ES6 spread syntax):

obj = { ... obj, count: obj.count + 1 };

OnPush

Checking the full component tree every time change detection runs could be very expensive. Using this strategy can help to avoid performance issues, as it ensures that change detection will only be run for a component (and its children) when one of its inputs changes.

ChangeDetectorRef.markForCheck() can be used to manually trigger change detection for such a component.

Good luck!

This is a very brief overview of Angular's change detection mechanism, but hopefully it's enough to give a basic understanding of how it works, and to help prevent some common pitfalls.

Further Reading

Published 2019/11/20