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.

Questions?

us, or ask in #media on Discord