angularjs infinite $digest Loop when no scope changes

All we need is an easy explanation of the problem, so here it is.

I’m getting the below error in my angular code. I’m struggling to understand why the function getDrawWithResults would cause a digest cycle as there don’t seem to be any side effects? It just returns items from a list that have a property set to true.

The error only occurs when the first use of getDrawWithResults is on the page, if I remove, the error stops.

Uncaught Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"]]

This is my code:

HTML

<h4 ng-cloak ng-hide="getDrawsWithResults(selectedLottery.draws)">Results of Selected Lottery</h4>

<div class="con" ng-repeat="draw in getDrawsWithResults(selectedLottery.draws)" ng-cloak>
    <h5 class="con__header">[[ draw.date|date:'EEEE d MMMM yyyy - H:mm' ]]</h5>
    <div class="balls-list__outer con__row">
        <div class="balls-list">
            <div class="balls-list__ball__outer" ng-repeat="b in draw.results">
                <button class="balls-list__ball btn btn-con">[[ b ]]</button>
            </div>

        </div>
    </div>
</div>

JS

// return list of draws with results
$scope.getDrawsWithResults = function(draws) {
    return _.filter(draws, function(draw){
        return draw.results && draw.results.length > 0;
    });
}

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

I assume _.filter returns a new array instance everytime it is run. This causes angular’s implicit $watches like:

ng-hide="getDrawsWithResults(selectedLottery.draws)"

and

ng-repeat="draw in getDrawsWithResults(selectedLottery.draws)"

to think that the model has changed so it needs to digest again.

I would implement a filter

app.filter('withResults', function() {
    return function(draws) {
        return _.filter(draws, function(draw){
            return draw.results && draw.results.length > 0;
        });
    }
})

and apply it like that (see EDIT below):

ng-hide="selectedLottery.draws | withResults"

and

ng-repeat="draw in selectedLottery.draws | withresults"

EDITED after discussion in comments

The actual problem is this binding:

ng-hide="getDrawsWithResults(selectedLottery.draws)"

ng-hide registers a watch which will fire forever since the reference of the filtered array always changes. It can be changed to:

ng-hide="getDrawsWithResults(selectedLottery.draws).length > 0"

and the corresponding filter:

ng-hide="(selectedLottery.draws | withResults).length > 0"

ng-repeat does not have the same problem because it registers a $watchCollection.

Method 2

This implies $scope.getDrawsWithResults() is not idempotent. Given the same input and state it doesn’t consistently return the same result. And ng-hide requires an idempotent function (as do all function that Angular has a watch on).

In short, you may be better off using a function that returns a single boolean result instead of _.filter which returns an array. Perhaps _.all?

The reason idempotence matters here is because of the way Angular’s $digest cycle works. Because of your ng-hide Angular places a watch on the results of your $scope.getDrawsWithResults(). This way it can be alerted whenever it should re-evaluate that ng-hide. Your ng-repeat is not affected because it’s results don’t need to be watched by Angular.

So every time a $digest happens (which is many times a second) $scope.getDrawsWithResults() is called to see if it’s results changed from the previous $digest cycle and thus whether it should change ng-hide. If the result has changed Angular knows that could also mean some other function it’s watching (which possibly uses a result from your function) could have changed. So it needs to re-run $digest (letting the change propagate through the system if need be).

And so $digest keeps running until the results of all functions it’s watching stop changing. Or until there’s been 10 $digest cycles. Angular assumes that if the system isn’t stable after 10 cycles it probably will never stabilise. And so it gives up and throws the error message you got.

You can dive into this all in more depth here if you’d like: http://teropa.info/blog/2013/11/03/make-your-own-angular-part-1-scopes-and-digest.html

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply