AngularJS ng-repeat, One Time Bindings & Track By

Another interesting lesson yesterday. It was necessary to track by a unique key, so I created a composite key…only to find out I didn’t need to.

A look at ng-repeat

First of all, let’s talk about ng-repeat. The directive is one everybody is familiar with. However, some don’t realize how it works. When a digest cycle occurs, it rerenders its children. It is pretty smart, though. If a developer does not define a track by statement, Angular will generate a $$hashKey property on each object. It checks whether or not the collection order changed, or the references changed by using that $$hashKey. This helps enhance the performance of the repeat, as it will only rerender the items that changed in order or the $$hashKey value changed.

One time bindings inside ng-repeat

There are two things that can be one time bound in an ng-repeat statement. This includes the collection itself (i.e. ng-repeat="item in ::Ctrl.collection) and the elements inside of the repeat.

The collection being one time bound will prevent any rerendering of the child elements as elements are added, removed, and reordered from the ng-repeat’d collection.

One time binding the contents of the element (see example below) work just fine when a repeated collection changes. This is because ng-repeat clears out its contents and recompiles the content if the array changes in any fashion.

Enter track by

track by can be utilized to do the same operation as above. It helps performance by not having to generate a $$hashKey for each element in the collection. The track by property must be unique. Developers can use any unique identifier on the objects, or using $index. The $index property is generated on the scope by the ng-repeat and is set to the index of the repeated collection.

This, however, has its issues.

What if we swap out the array reference?

This is the scenario I encountered. We have a collection of objects, each having a collection of objects that were being repeated over. When the reference to the outer object changed, it pointed to a new child array on that object.

What’s the problem with this? I was tracking by $index. From the above, it's possible to see why this is a problem...

When the collection reference that is being iterated over changed, it forced a digest cycle, but, the track by saw that the first repeated element was tracking by 0, which was still the first element in the collection (as 0 is the first index of the old and new collection). It didn't require a rerender because the index didn't change.

It partially covered the error by appending an extra element (reference 1 had 1 element in its collection, reference 2 had 2 elements). That is because there was no index of 1 element in the first reference, and therefore needed to be rendered.

Since I one-time bound the contents, the first element in the collection had its value on the screen even though the array reference changed. That is because Angular decided not to rerender the element because its index didn’t change.

The solution

One way around this was to generate a unique identifier on each sub collection of each selectable object. This, however, was ugly. I was appending the parent object’s identifier with the index to form 707734582-1 for the sole purpose of tracking. Annoying, but it works.

Front End Developer