Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with:

Using Track-By With ngRepeat In AngularJS 1.2

By Ben Nadel on

With the release of AngularJS 1.2 earlier this week, I have to say that the feature about which I am most excited is the "track by" augmentation for ngRepeat. This feature allows you to associate a JavaScript object with an ngRepeat DOM (Document Object Model) node using a unique identifier. With this association in place, AngularJS will not $destroy and re-create DOM nodes unnecessarily. This can have a huge performance and user experience benefit.


 
 
 

 
  
 
 
 

View this demo in my JavaScript-Demos project on GitHub.

As I've blogged about before, when AngularJS renders an ngRepeat list, it injects an expando property - $$hashKey - into your JavaScript objects. It then uses this $$hashKey to associate your objects with the rendered DOM nodes. In the past, I've the hashKeyCopier library to manually manage these $$hashKey's such that I could updated the rendered collection with live data without causing unnecessary (and sometimes harmful) DOM changes.

But, no more! With the new "track by" syntax, I can now tell AngularJS which object property (or property path) should be used to associate a JavaScript object with a DOM node. This means that I can swap out JavaScript objects without destroying DOM nodes so long as the "track by" association still works.

To see this in action, we're going to render and then re-render two different ngRepeat lists. The first list will use the vanilla ngRepeat syntax. The second list will use the "track by" syntax. Then, each list item will use a tracking directive that will log DOM node creation.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Using Track-By With ngRepeat In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Using Track-By With ngRepeat In AngularJS
  • </h1>
  •  
  •  
  • <h2>
  • Without Track-By
  • </h2>
  •  
  • <ul>
  • <li
  • ng-repeat="friend in friendsOne"
  • bn-log-dom-creation="Without">
  •  
  • {{ friend.id }} &mdash; {{ friend.name }}
  •  
  • </li>
  • </ul>
  •  
  •  
  • <h2>
  • With Track-By
  • </h2>
  •  
  • <!--
  • This time, we're going to use the same data structure;
  • however, we're going to use the "track by" syntax to tell
  • AngularJS how to map the objects to the DOM node.
  • --
  • NOTE: You can also use a $scope-based function like:
  • ... track by identifier( item )
  • -->
  • <ul>
  • <li
  • ng-repeat="friend in friendsTwo track by friend.id"
  • bn-log-dom-creation="With">
  •  
  • {{ friend.id }} &mdash; {{ friend.name }}
  •  
  • </li>
  • </ul>
  •  
  • <p>
  • <a ng-click="rebuildFriends()">Rebuild Friends</a>
  • </p>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.min.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // Set up the initial collections.
  • $scope.friendsOne = getFriends();
  • $scope.friendsTwo = getFriends();
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I rebuild the collections, creating completely new
  • // arrays and Friend object instances.
  • $scope.rebuildFriends = function() {
  •  
  • console.log( "Rebuilding..." );
  •  
  • $scope.friendsOne = getFriends();
  • $scope.friendsTwo = getFriends();
  •  
  • // Log the friends collection so we can see how
  • // AngularJS updates the objects.
  • setTimeout(
  • function() {
  •  
  • console.dir( $scope.friendsOne );
  • console.dir( $scope.friendsTwo );
  •  
  • },
  • 50
  • );
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I create a new collection of friends.
  • function getFriends() {
  •  
  • return([
  • {
  • id: 1,
  • name: "Sarah"
  • },
  • {
  • id: 2,
  • name: "Tricia"
  • },
  • {
  • id: 3,
  • name: "Joanna"
  • }
  • ]);
  •  
  • }
  •  
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I simply log the creation / linking of a DOM node to
  • // illustrate the way the DOM nodes are created with the
  • // various tracking approaches.
  • app.directive(
  • "bnLogDomCreation",
  • function() {
  •  
  • // I bind the UI to the $scope.
  • function link( $scope, element, attributes ) {
  •  
  • console.log(
  • attributes.bnLogDomCreation,
  • $scope.$index
  • );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link
  • });
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, all we have to do is tell the ngRepeat directive to use the "id" property to associate each Friend instance with a rendered DOM node. We can see the difference in behavior in the console output:


 
 
 

 
 Using track-by with ngRepeat in AngularJS 1.2. 
 
 
 

As you can see, during the "rebuild" action, in which we replaced the rendered collections, the vanilla ngRepeat list caused new DOM node creation with each rebuild. The "track by" ngRepeat list, on the other hand, caused no subsequent DOM activity, even when the underlying collection was replaced. This is because AngularJS knew how to associate each item with the corresponding DOM node.

Furthermore, you can see that an ngRepeat list with a "track by" clause doesn't inject the $$hashKey expando property.

I'm sure that people are frothing at the mouth to get "animation" in AngularJS 1.2. But, for me, I think this "track by" behavior is going to be the big win. When your applications get bigger and you start to deal with caching and mixing live data with cached data, the ease with which you can associate JavaScript objects with DOM nodes is going to have a huge performance payoff. Honestly, I would upgrade just for this feature.




Reader Comments

Great post, it's definitely a great feature of the new version.
One thing I realized when playing with it though is that when using filter in the repeat expression (item in items | filter:search track by item.id) it seems that elements are still destroyed and recreated in the DOM. so back to your previous post (discussing the use of ng-show instead of filtering) it is probably wise to use ng-show together with "track by".

Thanks

Reply to this Comment

Another very important thing I just realized:

When using Angular Resources (resource.query(...)), the return value is actually a "future" and not the data itself, so in case the ng-repeat is bound to a property in the $scope that is actually a future, when replacing that future with a new one (by reloading the data for example) all the items will be destroyed and recreated even when using the "track by" mechanism. my guess is that it's because the future object is basically empty initially and so the track by mechanism can't find any item until it's actually filled.

As a workaround I use the success callback of the resource.query method and just set the actual result back to the property in the $scope.

something like:

myResource.query({}, function (data) {
$scope.divisionItems = data;
});

Reply to this Comment

@Hadas,

Yeah, I think I remember reading that as one of the "breaking changes" in the 1.2 release (going from an empty object/array to a Promise for the resource return). To be honest, this won't affect me personally (in my current app) as we typically don't render data until is loaded. We usually have some sort of loading view:

  • <div ng-switch="isLoading">
  •  
  • <div ng-switch-when="true">
  • ...loading...
  • </div>
  •  
  • <div ng-switch-when="false">
  • ... render the data ...
  • </div>
  •  
  • </div>

That said, we DO deal a lot with locally-cached data and that's where I'm very interested to see if ngRepeat/track-by play nicely, which I suspect they will.

That said, my app is actually still running on AngualrJS 1.0.3 :( It makes me sad but we haven't had time to update yet.

Reply to this Comment

Thanks for this article, I actually had a problem where updates were affecting the object in drag & drop (the DOM element was deleted / recreated, and the dragging element was disappearing), and the "track by" fixed it ! :)

Reply to this Comment

@Bret,

Thanks for the tip. Filters (and filter expressions) are still something that trip me up. There's something that doesn't feel natural enough for it to be committed to my brain, for some reason.

Reply to this Comment

I was hoping that this will work with pagination using ng-repeat.

My use case scenario is that I have an images object.
images[0] = [im1,im2,im3] // First Page
images[1] = [im4, im5, im6] // Second page

I am displaying images in the table with pagination.

  •  
  • <tbody>
  • <tr bn-log-dom-creation="With" ng-repeat="image in images[currentPage] track by $index+currentPage*imagesPerPage" class="ng-scope">
  • <td>
  • {{image.details}}
  • </td>
  •  
  • </tr>
  • </tbody>
  • <div onclick="setPage(1)">1</div>
  • <div onclick="setPage(2)">2</div>
  •  
  • <script>
  • // Controller defines the images[] data object and following method
  • currentPage = 0
  • imagesPerPage = 5
  • images = []
  • images[0] = [..]
  • images[1] = [..]
  • function setPage (page) {
  • currentPage = page;
  • }
  •  
  • </script>

When currentPage variable is changed in scope, ng-repeat re renders the DOM using the new value of currentPage.

So my app has multiple pages.
When i move from page 1 to page 2 it creates a new DOM elements but when I move back to page 1 I want old DOM to be retained which doesn't happen.

ng-repeat again creates the DOM elements for page 1 even though I am using track by.

Can you please tell me what's wrong with my code.

Reply to this Comment

While I am using track by, and changed the value of specific key after render. It doesn't reflect the changes in UI. Not sure if its a bug or I have to write some code for that ? Can you please clearify.

Reply to this Comment

@Hadas,

Got the same problem and i can't manage to get it working...

messagesService.post({'content':msgContent,'email':'email@somewhere.com'},function(data) {
$scope.messages.push(data);
});

Unfortunaly data is an array for me... so i can't push it to the already fetched messages.

I mean the json response from the server is splitted into an array for each character ...
0=>"{",1=>"i",2=>"d" ... and so on instead of { "id"
any idea ?

The track by feature is a great idea, it's too bad that it doesn't works good with $resource as 90% of apps are pulling data from an API ( rest or other )

Reply to this Comment

I have 600 records and in first go I am trying to use limit(10) along with ng-repeat. Plus there can be duplicates so I used 'track by $index' too.
I observed it works perfect for <50 records but slows down for 600+.
Along with this I can see many line functions of angularjs created in debugger as below.

var p;
if(s == null) return undefined;
s=((k&&k.hasOwnProperty("selectall"))?k:s)["selectall"];
return s;

I really worries if ng-repeat is performance hit and why so many inline functions are created as we go on increasing use of directives.
As a workaround I am limiting max records to 100, but it would be helpful to know performance considerations here.

Reply to this Comment

Great post ...
Aside from ng-repeat performance improvement -- which I guess is the main point here -- when not destroying and creating scopes, this allows our scopes to keep track of previous states and implement state machines via watches, which just isn't possible with vanilla ng-repeat.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.