Tracked Properties

Tracked properties replace computed properties. Unlike computed properties, which require you to annotate every getter with the values it depends on, tracked properties only require you to annotate the values that are trackable, that is values that:

  1. Change over the lifetime of their owner (such as a component) and
  2. May cause the DOM to update in response to those changes

For example, a computed property like this:

import EmberObject, { computed } from '@ember/object';

const Image = EmberObject.extend({
  aspectRatio: computed('width', 'height', function() {
    return this.width / this.height;
  }),
});

Could be rewritten as:

import { tracked } from '@glimmer/tracking';

class Image {
  @tracked width;
  @tracked height;

  get aspectRatio() {
    return this.width / this.height;
  }
}

Notice how aspectRatio doesn't require any annotation at all - it's a plain old native getter, and it'll still work and invalidate if it's used anywhere in a template, directly or indirectly.

An additional benefit is that you no longer have to use set to update these values, you can use standard JavaScript syntax instead!

// Before
let profilePhoto = Image.create();
profilePhoto.set('width', 300);
profilePhoto.set('height', 300);
// After
let profilePhoto = new Image();
profilePhoto.width = 300;
profilePhoto.height = 300;

@tracked installs a native setter that tracks updates to these properties, allowing you to treat them like any other JS value.

Tracked properties have subtler benefits as well:

  • They enforce that all of the trackable properties in your classes are annotated, making them easy to find. With computed properties, it was common to have properties be "implicit" in a class definition, like in the example above; the classic class version of Image doesn't have width and height properties defined, but they are implied by their existence as dependencies in the aspectRatio computed property.
  • They enforce a "public API" of all values that are trackable in your class. With computed properties, it was possible to watch any value in a class for changes, and there was nothing you as the class author could do about it. With tracked properties, only the values you want to be trackable will trigger updates to anything external to your class.

Most computed properties should be fairly straightforward to convert to tracked properties. It's important to note that in these new components, arguments are automatically tracked, but in classic components they are not. This is because arguments are put on the args hash, which is tracked property. Since they are assigned to arbitrary properties on classic components, they can't be instrumented ahead of time, so you must decorate them manually.

Plain Old JavaScript Objects (POJOs)

It's not uncommon to use POJOs in Ember code for storing state, representing some models, etc. This works because get and set can be used for any path, on any object, whether or not its an EmberObject, and whether or not the property was declared in advance. This is part of what lead to the "implicit" property problem - you set any property you wanted on an existing object and it would work.

With tracked properties this is not possible, since each property must be instrumented ahead of time, and decorators can only be applied in classes. In general, the recommendation here is to convert usages of POJOs to native classes wherever possible:

// Before
import EmberObject, { computed } from '@ember/object';

const Person = EmberObject.extend({
  init() {
    this.address = {};
  },

  fullAddress: computed('address.{street,city,region,country}', function() {
    let { street, city, region, country } = this.address;

    return `${street}, ${city}, ${state}, ${country}`;
  }),
});
// After
import { tracked } from '@glimmer/tracking';

class Address {
  @tracked street;
  @tracked city;
  @tracked region;
  @tracked country;
}

class Person {
  address = new Address();

  get fullAddress() {
    let { street, city, region, country } = this.address;

    return `${street}, ${city}, ${state}, ${country}`;
  }
}

In some cases, if your usage of properties on POJOs is too dynamic, you may not be able to enumerate every single property that could be tracked. There could be a prohibitive number of possible properties, or there could be no way to know them in advance. In this case, it's recommended that you reset the value wherever it is updated:

class SimpleCache {
  @tracked _cache = {};

  set(key, value) {
    this._cache[key] = value;

    // trigger an update
    this._cache = this._cache;
  }

  get(key) {
    return this._cache[key];
  }
}

Triggering an update like this will cause any getters that used the _cache to recalculate. Note that we can use the get method to access the cache, and it will still push the _cache tracked property.

Arrays

Arrays are another example of a type of object where you can't enumerate every possible value - after all, there are an infinite number of integers (though you may run out of bits in your computer at some point!). Instead, you can continue to use EmberArray, which will continue to work with tracking and will cause any dependencies that use it to invalidate correctly.

import { A } from '@ember/array';

class ShoppingList {
  items = A([]);

  addItem(item) {
    this.items.pushObject(item);
  }
}

Backwards Compatibility

Tracked properties are fully backwards compatible with computed properties and get/set. Computed properties can depend on tracked properties like any other dependency:

import { tracked } from '@glimmer/tracking';
import { computed } from '@ember/object';

class Image {
  @tracked width;

  @computed('width', 'height')
  get aspectRatio() {
    return this.width / this.height;
  }
}

let profilePhoto = new Image();

// This will correctly invalidate `aspectRatio`
profilePhoto.width = 200;

Note, however, that if you want to use a getter as a dependent key, you will need to use the dependentKeyCompat decorator. This allows you to refactor existing computed properties into getters without breaking existing code that observes them.

Vice-versa, computed properties used in native getters will autotrack and cause the getter to update correctly:

class Image {
  @computed('width', 'height')
  get aspectRatio() {
    return this.width / this.height;
  }

  get helloMessage() {
    return `Image aspect ratio is: ${this.aspectRatio}!`;
  }
}

Likewise, properties that are not decorated with @tracked that you get using get will also autotrack, and update later on when you use set to update them:

class Image {
  get aspectRatio() {
    let width = get(this, 'width');
    let height = get(this, 'height');

    return width / height;
  }
}

let profilePhoto = new Image();
set(profilePhoto, 'width', 300);
set(profilePhoto, 'height', 300);

However, you must use get for these properties, since they are not tracked and there is no way to know in advance that they might be changed with set. For instance, this will not work:

class Image {
  get aspectRatio() {
    return this.width / this.height;
  }
}

let profilePhoto = new Image();
set(profilePhoto, 'width', 250);
set(profilePhoto, 'height', 250);

Additionally, certain Ember objects still require the use of get and set, such as ObjectProxy and ArrayProxy. These will continue to function with tracked, but you must use get and set. Likewise, KVO methods on Ember's Enumerable class, such as objectAt and pushObject, and the various implementations of it will generally continue to be tracked.

If you have implemented your own version of an Ember Enumerable, or the EmberArray mixin, in general, you will need to add an additional step to your implementation of objectAt in order for it to work with tracking:

objectAt() {
  get(this, '[]');

  // your implementation
}

This will push the tag for the [] property onto the autotrack stack, and that property is what is invalidated when the array is updated with KVO methods.

When to Use get and set

Ember's classic change tracking system used two methods to ensure that all data was accessed properly and updated correctly: get and set.

import { get, set } from '@ember/object';

let image = {};

set(image, 'width', 250);
set(image, 'height', 500);

get(image, 'width'); // 250
get(image, 'height'); // 500

In classic Ember, all property access had to go through these two methods. Over time, these rules have become less strict, and now they have been minimized to just a few cases. In general, in a modern Ember app, you shouldn't need to use them all that much. As long as you are marking your properties as @tracked, Ember should automatically figure out what needs to change, and when.

However, there still are two cases where you will need to use them:

  • When accessing and updating plain properties on objects without decorators
  • When using Ember's ObjectProxy class, or a class that implements the unknownProperty function (which allows objects to intercept get calls)

Additionally, you will have to continue using accessor functions for arrays if you want arrays to update as expected. These functions are covered in more detail in the Looping Through Lists guide.

Importantly, you do not have to use get or set when reading or updating computed properties, as was noted in the computed property section.

Plain Properties

In general, if a value in your application could update, and that update should trigger rerenders, then you should mark that value as @tracked. This oftentimes may mean taking a POJO and turning it into a class, but this is usually better because it forces us to rationalize the object - think about what its API is, what values it has, what data it represents, and define that in a single place.

However, there are times when data is too dynamic. As noted below, proxies are often used for this type of data, but usually they're overkill. Most of the time, all we want is a POJO.

In those cases, you can still use get and set to read and update state from POJOs within your getters, and these will track automatically and trigger updates.

class Profile {
  photo = {
    width: 300,
    height: 300,
  };

  get photoAspectRatio() {
    return get(this.photo, 'width') / get(this.photo, 'height');
  }
}

let profile = new Profile();

// render the page...

set(profile.photo, 'width', 500); // triggers an update

This is also useful when working with older Ember code which has not yet been updated to tracked properties. If you're unsure, you can use get and set to be safe.

ObjectProxy

Ember has and continues to support an implementation of a Proxy, which is a type of object that can wrap around other objects and intercept all of your gets and sets to them. Native JavaScript proxies allow you to do this without any special methods or syntax, but unfortunately they are not available in IE11. Since many Ember users must still support IE11, Ember's ObjectProxy class allows us to accomplish something similar.

The use cases for proxies are generally cases where some data is very dynamic, and its not possible to know ahead of time how to create a class that is decorated. For instance, ember-m3 is an addon that allows Ember Data to work with dynamically generated models instead of models defined using @attr, @hasMany, and @belongsTo. This cuts back on code shipped to the browser, but it means that the models have to dynamically watch and update values. A proxy allows all accesses and updates to be intercepted, so m3 can do what it needs to do without predefined classes.

Most ObjectProxy classes have their own get and set method on them, like EmberObject classes. This means you can use them directly on the class instance:

proxy.get('width');
proxy.set('width', 100);

If you're unsure whether or not a given object will be a proxy or not, you can still use Ember's get and set functions:

get(maybeProxy, 'width');
set(maybeProxy, 'width', 100);

© 2020 Yehuda Katz, Tom Dale and Ember.js contributors
Licensed under the MIT License.
https://guides.emberjs.com/v3.25.0/upgrading/current-edition/tracked-properties