solving $rootScope:infdig Infinite $digest Loop

lostdorje picture lostdorje · Jul 9, 2014 · Viewed 12.9k times · Source

I get the basic idea of the infinite digest loop and how it happens, but I'm running into the problem. Here is a fiddle demonstrating my code and problem:

http://jsfiddle.net/eS5e5/1/

In the jsfiddle console you'll see the infinite digest loop.

Basically I have to make decisions on data that may not have loaded yet so I need to wait for the promise to resolve using then(). I have a promise called user. There are two different places in the code where I call then() on user.

  1. Just after I define it. I need to set a scope variable based on it.
  2. In another scope method, $scope.isAdmin()

For number 2, it might be asked why I just don't use $scope.user directly in the $scope.isAdmin() method. The problem is, it's possible for $scope.isAdmin() to be called before the async request for the user returns, in which case I need to 'block' before returning from $scope.isAdmin().

My question is, what about $scope.isAdmin() is making angular think that a 'watched' variable has changed and that the digest cycle needs to run again?

$scope.isAdmin() isn't actually changing anything.

Here is the stripped down code:

HTML:

<body ng-controller='myController'>  
  <div ng-if='isAdmin()'>Hi! <strong>{{ user.username }}</strong> is an Admin!!!</div>
  <div ng-if='!isAdmin()'>Hi! <strong>{{ user.username }}</strong> is NOT an Admin!!!</div>
</body>

And the JS:

angular.module('myApp', [])
  .factory('myService', function($q, $timeout) {
    return {        
      getUser: function() {
        var deferred = $q.defer();

        $timeout(function() {
          deferred.resolve({ username: 'me', isAdmin: true });
        }, 2000);

        return deferred.promise;
      }
    };
  })
  .controller('myController', function($scope, $q, myService) {      
    var getUserDeferred = $q.defer();
    var user = getUserDeferred.promise;
    user.then(function(user) {
      $scope.user = user;
      return user;
    });

    $scope.getUser = function() {
      return myService.getUser().then(function(user) {
        getUserDeferred.resolve(user);
      });
    };

    $scope.isAdmin = function() {
      return user.then(function(user) {
        return user.isAdmin;
      });
    };

    $scope.getUser();
  });

Answer

lostdorje picture lostdorje · Jul 9, 2014

So I finally figured out my own problem and thought I would answer it for others in case someone else may find this info useful.

The crux of the fix had to do with 2 concepts: angular promises and angular watches. By being aware of and applying the 2 concepts together the fix was actually pretty simple.

Everything you put on $scope is 'watched' including functions. Every time something watched changes $scope.$apply() runs again to apply the changes. If a scope function (eg: $scope.isAdmin()) changes its return value from one 'apply' to the next it will trigger another 'apply', until things stabilize and the return value isn't changing.

But in my code I was returning user.then(...) which just returns a new promise (which kept the apply cycle going on forever since the return value kept changing).

In my isAdmin() function I needed to defer its return value until the user actually loaded (any other return value would be meaningless). So I changed the code to check if the user async call had resolved by checking $scope.user and if so returning a valid isAdmin value. If $scope.user was still not defined I would just return the promise I already created.

I changed the $scope.isAdmin() to be:

$scope.isAdmin = function() {
  if ($scope.user) {
    return $scope.user.isAdmin;
  }

  return user;
};

This has the same effect as the original code without triggering an infinite apply cycle. Specifically, if the $scope.user has not resolved we still return a promise as before, by returning the user var. Note however that the user var is the same promise not a new one created by then() so the apply cycle stabilizes.

And just for completeness here is the updated jsfiddle:

http://jsfiddle.net/eS5e5/2/