116

I want to create divs repeatedly, the items is objects returned by a function. However the following code report errors: 10 $digest() iterations reached. Aborting! jsfiddle is here: http://jsfiddle.net/BraveOstrich/awnqm/

<body ng-app>
  <div ng-repeat="entity in getEntities()">
    Hello {{entity.id}}!
  </div>
</body>
Devs love ZenUML
  • 11,344
  • 8
  • 53
  • 67

4 Answers4

200

Short answer: do you really need such function or you can use property? http://jsfiddle.net/awnqm/1/

Long answer

For simplicity I will describe only your case - ngRepeat for array of objects. Also, I'll omit some details.

AngularJS uses dirty checking for detecting changes. When application is started it runs $digest for $rootScope. $digest will do depth-first traversal for scope's hierarchy. All scopes have list of watches. Each watch has last value (initially initWatchVal). For each scope for all watches $digest runs it, gets current value (watch.get(scope)) and compares it to watch.last. If current value is not equal to watch.last (always for first compare) $digest sets dirty to true. When all scopes are processed if dirty == true $digest starts another depth-first traversal from $rootScope. $digest ends when dirty == false or number of traversals == 10. In the latter case, the error "10 $digest() iterations reached." will be logged.

Now about ngRepeat. For each watch.get call it stores objects from collection (returning value of getEntities) with additional information in cache (HashQueueMap by hashKey). For every watch.get call ngRepeat tries to get object by its hashKey from cache. If it does not exist in cache, ngRepeat stores it in cache, creates new scope, puts object on it, creates DOM element, etc.

Now about hashKey. Usually hashKey is unique number generated by nextUid(). But it can be function. hashKey is stored in object after generating for future use.

Why your example generates error: function getEntities() always returns array with new object. This object doesn't have hashKey and doesn't exist in ngRepeat cache. So ngRepeat on each watch.get generates new scope for it with new watch for {{entity.id}}. This watch on first watch.get has watch.last == initWatchVal. So watch.get() != watch.last. So $digest starts new traverse. So ngRepeat creates new scope with new watch. So ... after 10 traverses you get error.

How you can fix it

  1. Do not create new objects on every getEntities() call.
  2. If you need to create new objects you can add hashKey method for them. See this topic for examples.

Hope people who know AngularJS internals will correct me if I'm wrong in something.

levvy
  • 5
  • 2
Artem Andreev
  • 19,942
  • 5
  • 43
  • 42
  • 4
    +1 thank you for this. I had the same issue and could not use a static property for it. $$hashKey should really be documented on the ngRepeat page of the manual IMO. – Michael Moussa Feb 01 '13 at 02:12
  • Any idea what changed from 1.1.3 to 1.1.4 that affected this? Prior to 1.1.4 this actually worked. There's nothing in the changelog about it and I can't reason out what the difference is. The current behavior makes sense. – m59 Jan 05 '14 at 17:42
  • Also, check this out if you could: http://stackoverflow.com/questions/20933261/angularjs-infdig-error-infinite-loop-with-ng-repeat-function-that-returns-arra I'm not sure if my answer is the way to go or not.. – m59 Jan 05 '14 at 18:45
  • @m59 I believe this is related to the addition of `$watchCollection()` in 1.1.4. `$watchCollection()` naively compares the array items (using `===`) in contrast to `$watch()` which compared them using `equals()`. – urish Apr 20 '14 at 18:10
  • 2
    So following the recommendation to `Do not create new objects on every getEntities() call.` it can be fixed pretty easily like this: `
    `
    – przno May 20 '14 at 12:47
  • 2
    the solution from my previous comment works in case when `getEntities()` always returns the same array, if the array ever change, you will not get it in the `ng-repeat` – przno May 20 '14 at 12:59
45

Initialise the array outside of the repeat

<body ng-app>
   <div ng-init="entities = getEntities()">
       <div ng-repeat="entity in entities">
           Hello {{entity.id}}!
       </div>
   </div>
</body>
Mwayi
  • 1,623
  • 15
  • 13
  • 9
    This does not work if the `getEntities()` returns something different in the lifecycle of the program. Say, for instance, that `getEntities()` trigger an `$http.get`. When the get finally resolve (you made the AJAX call), `entities` will already be initialized. – Nighto Oct 04 '14 at 23:24
  • 3
    From the angular docs `The only appropriate use of ngInit is for aliasing special properties of ngRepeat. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.` – Blowsie Apr 09 '15 at 13:39
  • I think the main point is to "initialise the array outside of the repeat" by whatever means... and @Nighto good call on promises – Mwayi Dec 28 '16 at 20:51
16

This was reported here and got this response:

Your getter is not idempotent and changes the model (by generating a new array each time it is called). This is forcing angular to keep on calling it in hope that the model will eventually stabilize, but it never does so angular gives up and throws an exception.

The values the getter return are equal but not identical and that's the problem.

You can see this behavior go away if you move the array outside the Main controller:

var array = [{id:'angularjs'}];
function Main($scope) {
    $scope.getEntities = function(){return array;};
};

because now it is returning the same object each time. You may need to re-architect your model to use a property on the scope instead of a function:

We worked around it by assigning the result of the controller's method to a property, and doing ng:repeat against it.

Dennis
  • 32,200
  • 11
  • 64
  • 79
8

Based on @przno comment

<body ng-app>
  <div ng-repeat="item in t = angular.equals(t, getEntities()) ? t : getEntities()">
    Hello {{item.id}}!
  </div>
</body>

BTW second solution @Artem Andreev suggests is not working in Angular 1.1.4 and greater, while first one does not solve the problem. So, I'm afraid for now this is the less spiky solution without disadvantages in functionality

Agat
  • 358
  • 2
  • 10