Michael Cranston

$onChanges & Deep Watches

TL;DR

$onChanges will not fire just because a bindable object changes. If you need to know every time the object changes, you will have to pass in a new/copied version of the object, or use $doCheck. Here is a live example on plnkr.co.

The Problem

Imagine we have a data object that gets passed into a child component:

$ctrl.data = {color: blue};
angular
  .module('app')
  .component('child', {
    bindings: {
      data: '<'
    },
    controller: class {
      $onChanges(changes) {
        // this method will only fire if this.data is a new object
      }
    }
  });

Whenever the color property changes, we want to run a function. The problem is that the $onChanges event will not fire because the object itself has not changed.

Three Possible Solutions

1. In your parent, pass in a copied version of this.data every time by using angular.copy(this.data).

2. Add the $doCheck hook to your component which will fire anytime the scope of your component changes. The downside of this approach is that it could be a performance hit.

angular
 .module('app')
 .component('child', {
   bindings: {
     data: '<'
   },
   controller: class {
     $doCheck(changes) {
       // this method will fire anytime the component's scope changes
     }
   }
 });

3. You can still technically add $scope.$watch to your $onInit method, but this is a bad solution. $scope.$watch is not apart of Angular 2 and relying on it is counter-productive to component-based architecture and any upgrade strategy.

 angular
   .module('app')
   .component('child', {
     bindings: {
       data: '<'
     },
     controller: class {
       constructor($scope) {
         this.$scope = $scope;
       }

       $onInit() {
         $scope.$watch('$ctrl.data', function(value) {
           // do something
         }, true);
       }
     }
   });
 

Why $onChanges does not do deep watches

This is by design:

This is indeed intended. It would be too expensive to check each object deeply for changes. Besides, in most cases, where bindings are used in a template, an “internal” change will automatically update the view. If you want to account for “deep” changes, you need to manually $watch the object.

Further Reading

comments powered by Disqus