Tracked Properties (3.13)
Learn about the new way to access and mutate state in Ember – with vanilla JavaScript!
Summary
Classic Ember Components look something like this:
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
isOpen: false,
toggle: action(function () {
this.set("isOpen", !this.isOpen);
}),
});
<div>
<button {{on 'click' this.toggle}}>I'm a panel</button>
{{#if this.isOpen}}
<p>Lorem ipsum dolor...</p>
{{/if}}
</div>
We set properties on our components, and then when we want to mutate them, we need to call this.set
, as in the toggle
action above:
this.set("isOpen", !this.isOpen);
Originally, we also had to use this.get
in order to access state in Ember:
this.set("isOpen", !this.get("isOpen"));
but for a while we've been able to drop using this.get
(in most cases).
Now, we get to drop using this.set
! And tracked properties are the reason why.
Let's re-implement this panel using tracked properties. First we'll import tracked
and decorate our isOpen
state:
import Component from "@ember/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default Component.extend({
isOpen: tracked({ value: false }),
});
Here we're marking isOpen
as a tracked property. Note that once you are on Octane you will primarily be using tracked
as a decorator, which is much more ergonomic:
export default class extends Component {
@tracked isOpen = false;
}
but it's nice to understand it's use as a regular function on a classic component.
Now that we've marked isOpen
as a tracked property, we can implement our toggle
action using vanilla JavaScript access and mutation:
import Component from "@ember/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default Component.extend({
isOpen: tracked({ value: false }),
toggle: action(function () {
this.isOpen = !this.isOpen;
}),
});
And we're done! Tracked properties allow us to use vanilla JavaScript access and mutation patterns – like the assignment =
operator – on our Ember component state.
Let's add one more piece of state: the amount of times the panel's been clicked. We'll increment it in the toggle
action.
import Component from "@ember/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default Component.extend({
isOpen: tracked({ value: false }),
clicked: tracked({ value: 0 }),
toggle: action(function () {
this.isOpen = !this.isOpen;
this.clicked++;
}),
});
And we're done! Again, we can use native JavaScript syntax, like the ++
increment operator, to mutate our Ember component state.
Finally, Tracked Properties allow us to use native JavaScript getters as a replacement for computed properties.
A classic computed property that aliases isOpen
might look like this:
import Component from "@ember/component";
import { action, computed } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default Component.extend({
isOpen: tracked({ value: false }),
isOpen2: computed("isOpen", function () {
return this.isOpen;
}),
toggle: action(function () {
this.isOpen = !this.isOpen;
}),
});
As always, we had to specify isOpen
as a dependent key, so our computed property knew the update whenever the isOpen
state changed.
Here's the new version, as a native ES getter:
import Component from "@ember/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default Component.extend({
isOpen: tracked({ value: false }),
get isOpen2() {
return this.isOpen;
},
toggle: action(function () {
this.isOpen = !this.isOpen;
}),
});
If you've never seen ES getters before, they allow you to define functions that run whenever the property is looked up. And as you can see... we don't have to specify any dependent keys! Everything Just Works, thanks to our declaration that isOpen
is a tracked property.
So, Tracked Properties are a fundamental change in how Ember handles state, important to the Octane programming model, and move the entire Ember community farther from framework-specific APIs and microsyntaxes and closer to native JavaScript language constructs.