Friday, July 22, 2016

Component Lifecycle: $doCheck (angular 1.5.x)

Since angular 1.5 components got introduced together with a well defined lifecycle. Currently their are 4 hooks you can use in angular 1 components:

  • $onInit
  • $onChanges
  • $postLink
  • $onDestroy

$doCheck

In version 1.5.8 a new hook is introduced: $doCheck. And this is the equivalent of the angular 2 ngDoCheck implementation. It also serves the same purpose as the $onChanges, allow to act on changes made to the bindable fields of a component. As $onChanges uses the built-in change detection of angular, the $doCheck implementation is totally up to you. The hook is called for every digest cycle of the component and just let’s you know you should check your bindings on changes so you can act on it.

Usage

One of the case this could be useful is when you make use of the one-way (<) binding for passing objects. In this case the $onChanges hook will be called if the reference of the object changes and not when fields on the object it self change. So currently you had 2 possibilities to solve this:

  1. Always make sure you are passing a new object. This way $onChanges hook will be called for every change because the reference of the object will change from time to time.
  2. Add a watch on the object to keep track of the changes. This also means you need to destroy and recreate the the watch every the reference of the object changes and you have an (unwanted) dependency on $scope inside your component.
   1: module.component("component",{
   2:     template: "<div>{{$ctrl.item}}</div>",
   3:     bindings: {
   4:         inputItem: "<item"
   5:     },
   6:     controller: ["$scope", function($scope){
   7:         var $ctrl = this;
   8:         var destroyWatch;
   9:         this.$onChanges = function(changeObj){
  10:             if(changeObj.inputItem){
  11:                 this.item = 
  12:                   angular.copy(changeObj.inputItem.currentValue);
  13:                 if(destroyWatch) destroyWatch();
  14:                 destroyWatch = $scope.watch(function (){ 
  15:                     return changeObj.inputItem.currentValue 
  16:                 }, function (){ /* handle Changes */ })
  17:             }
  18:         }
  19:     }
  20: }]);

The $doCheck hook now adds a third possibility to solve this issue. By checking manually if the object has changed you can act on it. This can be done by storing the passed value into a local variable, so it can be used in the next call as previous value for comparison.

   1: module.component("component",{
   2:     template: "<div>{{$ctrl.item}}</div>",
   3:     bindings: {
   4:         inputItem: "<item"
   5:     },
   6:     controller: function(){
   7:         var $ctrl = this;
   8:         var previousInputItem;
   9:         this.$doCheck = function(){
  10:             if(!angular.equals(previousInputItem, this.inputItem)){
  11:                 previousInputItem = this.inputItem;
  12:                 this.item = angular.copy(this.inputItem);
  13:             }
  14:         }
  15:     }
  16: });

Performance

Change detection in angular 1.x is done using digest cycles and for every cycle the $doCheck hook will be called. This means this will be called a lot. This is why you have the be careful using this hook so it doesn’t cause any performance issues. Also keep in mind that any change made to the model inside the $doCheck hook will trigger a new digest cycle. If implemented wrong this can result into a loop of digest cycles.

In angular 2 the change is implemented on a different (more performant) way and this will result in less calls of the ngDoCheck. It will also throw an error if you trigger changes outside of the component in prod mode.

Conclusion

The $doCheck hook gives you the ability to take the change detection of the bindable fields into your own hands. This makes it possible to detect changes inside objects and arrays without having to change the reference. Ditto for detecting changes in date objects.

The $onChanges and $doCheck hook can easily live side-by-side. The $doCheck hook doesn’t effect the $onChanges hook at all. Of course fields checked in the $doCheck hook no longer need to be handled in the $onChanges hook. In angular 2 it’s even recommended to not use these 2 hooks together.