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:
- Change over the lifetime of their owner (such as a component) and
- 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 havewidth
andheight
properties defined, but they are implied by their existence as dependencies in theaspectRatio
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}, ${region}, ${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}, ${region}, ${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;
And 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:
import { get, set } from '@ember/object';
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:
import { set } from '@ember/object';
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 theunknownProperty
function (which allows objects to interceptget
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 guide on arrays (LINK TO ARRAY GUIDES HERE).
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);