Tuesday, August 25, 2015

Getting Inside Angular: $scope.$applyAsync

Have you ever used $scope.$apply and received a "$digest already in progress" error? If so, $scope.$applyAsync might be what you should be using!


What is $apply actually doing?

The most common reason to use $apply is when something is happening outside the scope of Angular and you want Angular to react to any changes that have occurred. $apply is your way of instructing Angular that something has happened and that it needs to go through its change detection routine--in other words, perform another digest. You might have noticed that "digest" was also mentioned in error message. This is no coincidence.

When you call $apply, it first executes the function that was supplied and then immediately starts a new digest cycle. In preparation to start a new digest, it checks that one is not already in progress before doing anything and if one is, the above exception is thrown. This can be somewhat jarring because you have little control over when Angular is performing a digest.

What’s the solution?

A common solution that I've seen is based on the fact that the function you want to $apply doesn't actually have to be "applied" at this moment in time and can wait to be executed at some point in the very near future. This is achieved by wrapping the $apply statement in a setTimeout with a very small timeout value, normally zero:

setTimeout(function () {

  $scope.$apply(function () {
   
    console.log('This will also work as you are using setTimeout');
  });
}, 0);

While this is all and good and will solve this issue in the majority of cases, there happens to be a more efficient mechanism already built into angular: $scope.$applyAsync!

Note: If the supplied function needs to been executed during the already in progress digest cycle, $evalAsync is probably what you're looking for.

$scope.$applyAsync

$applyAsync looks the same as $apply, but instead of starting a new digest immediately, it will schedule one to start in the very near future. The benefit of using $applyAsync over wrapping $apply in your own timeout, is when another digest cycle starts before the one that you scheduled does. If this happens, then the function supplied to $applyAsync will be executed before the change detection routine starts; when this wouldn't be the case If you had used your own timeout. The order of execution is up to the browser to decide what is next so by using $applyAsync, you are guaranteed that your function will be executed during the next new digest.

Example:

app.controller('MainCtrl', function($scope) {
  
  $scope.$watch(function () { },
    function (newValue, oldValue, scope) { 
      
      scope.$apply(function () {
        
        console.log('This will not work');
      });
    }
  );
    
  $scope.$watch(function () { },
    function (newValue, oldValue, scope) { 
      
      scope.$applyAsync(function () {
        
        console.log('This will work as you are using $applyAsync!');
      });
    }
  );
}); 

View on Plunker (open the developer tool's console).

The above code shows one example where a $apply is being called during a digest cycle which will result in a "$digest already in progress" error (open the developer tool's console to see this). The second example uses $applyAsync and the message "This will work as you are using $applyAsync!" will appear in the console.

//Code, rinse, repeat

4 comments:

  1. I'm personally using $timeout for that purpose, which works fine too.

    ReplyDelete
    Replies
    1. Ah yes, the $timeout service would be preferred over setTimeout for better testability.

      Delete