Here is how you can use trackBy with a property name

Ingo Bürk
Jun 12, 2018 · 4 min read

If you have written a production-ready Angular application before, you probably know that when displaying a list of data — at least somewhat large lists — you should be using Angular’s trackBy feature which looks something like

More often than not, we get the data from the backend and the information uniquely identifying an item is always the same, such as a property named id . Unfortunately, Angular forces us to write a tracking function in each component in which we want to make use of trackBy .

Wouldn’t it be nice if we could just handle this entirely in the template by passing a property like this?

Or — better yet — have something like the following so that we don’t even need to duplicate the 'id' in each and every component?

Hint: If you’d just like to see the result, you can see it in action here

So how could we make the above work? First of all, it’s important to understand how this syntax even works so that we can find a way to extend it. If you’re not much interested in this part, feel free to jump ahead to the next section.

The syntax used in the ngFor directive is microsyntax, which is briefly (but for our purposes sufficiently) documented:

The microsyntax parser takes of and trackBy , title-cases them […], and prefixes them with the directive’s attribute name (ngFor ), yielding the names ngForOf and ngForTrackBy . Those are the names of two NgFor input properties.

In other words, the following two snippets are equivalent and in fact this is how Angular will translate the structural directive initially:

Looking at ngFor ‘s code, we can actually see that the tracking function is simply an input named ngForTrackBy .

Hint: This is also how the »of« keyword works, by transforming it to the ngForOf directive. »let … of …« isn’t some built-in Angular language, but instead follows the same rules!

We can now formulate a plan on how to implement our own »keyword«: we simply have to create a directive with a ngForTrackById input.

Unfortunately, we are forced to prefix the name with »ngFor«, which we’d otherwise really like to avoid. Initially we might think that we could just opt for a slightly different syntax such as

but this won’t work as our directive will be placed on the wrong element when the microsyntax is desuggarized:

Another question to to ask here is which selector our directive should use. Initially, it might sound like a good idea to do it like this:

Hint: From here on out, we use the T generic to refer to a type which has an id field

The selectors used here are the same as NgForOf uses itself, and we activate our directive by waiting for our input to be set. However, down the road this won’t work as expected because the input will only be set after those of NgForOf . This poses a problem because — as of Angular 6.0.4 — NgForOf doesn’t handle changes to the tracking function. The problem is actually still not fatal (without going into further detail here), but it’s just not ideal.

We can instead simply use [ngTrackById] as our selector; afterall, we have just seen that Angular simply translates the microsyntax into this directive. This gives us the following skeleton to work with:

What we know by now is that somehow we want to overwrite the tracking function of the NgForOf programmatically from within our own directive. For the reasons I briefly outlined above, we can’t use any lifecycle hooks for this, but instead rely on the constructor as it will run before the ngForTrackBy input on the NgForOf directive is processed.

The way we gain access to the NgForOf directive is by simply injecting it. We also add the @Host() decorator as we’re only interested on the host element:

Now all that is left to do is to simply overwrite the tracking function here:

ngForTrackById — Final Directive

And, for a first version, that’s it! Let’s see this in action.

Putting It To The Test

In the following example, the items flash briefly upon creation and also display their age in seconds (please excuse the crude styling — I’m not a designer).

Reference Example Demonstrating a Missing Tracking Function

As we can see, clicking the button lights up all the items and resets their age as we’re missing any tracking function. Now let’s introduce our directive and compare:

Working Example with ngForTrackById

As we can see, pressing the button no longer recreates the items (even though we overwrite the list of items); et voilá — the tracking function is correctly applied!

Making It More Flexible

This ngForTrackById directive is nice and useful, provided all your data has an id field. But that might not always be the case and instead you’d like to just specify a property name.

Hint: Use caution when using this version of the directive. Specifying property names as strings breaks the type safety we get when using an actual tracking function.

We simply adapt our solution (and rename the directive in the process) to utilize a property passed in via an input.

ngForTrackByField — Final Directive

Note that since it’ll take some time for the input to be set and our function might be called before this happens, we fall back to returning the entire item. This seems like it might cause an unnecessary round-trip, but that’s not true: it’ll only happen the first time the data is rendered, and at this point in time Angular will have to create all items anyway.

All code in this story is released under the MIT license, the same as Angular itself.

Ingo Bürk

Written by

I'm a software consultant from Munich, Germany. My work involves full-stack development from Java to Angular.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade