home
  • Blog
5.11
  • Introduction
  • Getting Started
  • Tutorial
  • Core Concepts
  • Components
  • Routing
  • Services
  • EmberData
  • In-Depth Topics
    • Autotracking In-Depth
    • Patterns for Components
    • Patterns for Actions
    • Making API Requests
    • Native Classes In-Depth
    • Rendering Values
  • Application Development
  • Application Concerns
  • Accessibility
  • Configuration
  • Testing
  • Addons and Dependencies
  • Using TypeScript
  • Developer Tools
  • Ember Inspector
  • Code Editors
  • Additional Resources
  • Upgrading
  • Contributing to Ember.js
  • Glossary
Old Guides - You are viewing the guides for Ember v5.11.0.
Go to v6.3.0

Autotracking In-Depth


Autotracking is how Ember's reactivity model works - how it decides what to rerender, and when. This guide covers tracking in more depth, including how it can be used in various types of classes, and how it interacts with arrays and POJOs.

Autotracking Basics

When Ember first renders a component, it renders the initial state of that component - the state of the instance, and state of the arguments that are passed to it:

app/components/hello.hbs
{{this.greeting}}, {{@name}}!
app/components/hello.js
import Component from '@glimmer/component';

export default class HelloComponent extends Component {
  language = 'en';

  get greeting() {
    switch (this.language) {
      case 'en':
        return 'Hello';
      case 'de':
        return 'Hallo';
      case 'es':
        return 'Hola';
    }
  }
}
app/templates/application.hbs
<Hello @name="Jen Weber">

When Ember renders this template, we get:

Hello, Jen Weber!

By default, Ember assumes that none of the values that are rendered will ever change. In some cases this is clearly true - for instance, the punctuation in the template will always be the same, so Ember doesn't need to do anything to update it. These are static, state-less parts of the template. In other cases, like this.greeting or @name argument, that's less clear. It appears language might be something we want to update, and if we do, then greeting should probably change, right? At the least, we should check to see if it should change.

In order to tell Ember a value might change, we need to mark it as trackable. Trackable values are values that:

  1. Can change over their component’s lifetime and
  2. Should cause Ember to rerender if and when they change

We can do this by marking the field with the @tracked decorator:

app/components/hello.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class HelloComponent extends Component {
  @tracked language = 'en';

  get greeting() {
    switch (this.language) {
      case 'en':
        return 'Hello';
      case 'de':
        return 'Hallo';
      case 'es':
        return 'Hola';
    }
  }
}

When Ember renders a value like {{this.greeting}} in the template, it takes note of any tracked properties that it encounters, in this case language. If these values change in the future, it schedules a rerender, and then updates only the values that could have changed. This means that when language changes, only the Hello text in the browser will rerender - Ember leaves the , Jen Weber! portion completely alone!

Arguments, like {{@name}}, are automatically tracked, so if they change and are used somewhere in your component, the component will update accordingly.

Updating Tracked Properties

Tracked properties can be updated like any other property, using standard JavaScript syntax. For instance, we could update a tracked property via an action, as in this example component.

app/components/hello.hbs
{{this.greeting}}, {{@name}}!

<select {{on "change" this.updateLanguage}}>
  <option value="en">English</option>
  <option value="de">German</option>
  <option value="sp">Spanish</option>
</select>
app/components/hello.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class HelloComponent extends Component {
  @tracked language = 'en';

  get greeting() {
    switch (this.language) {
      case 'en':
        return 'Hello';
      case 'de':
        return 'Hallo';
      case 'es':
        return 'Hola';
    }
  }

  @action
  updateLanguage(event) {
    this.language = event.target.value;
  }
}

Now, whenever we change the value of the select, it'll call the action method, which will set the value of language. Since language is marked as tracked, and was used in rendering greeting, Ember will know that greeting needs to be re-rendered in the template, and will update.

Another way that a tracked property could be updated is asynchronously, if you're sending a request to the server. For instance, maybe we would want to load the user's preferred language:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class HelloComponent extends Component {
  constructor() {
    super(...arguments);

    fetch('/api/preferences')
      .then(r => r.json()) // convert the response to a JS object
      .then(response => {
        this.language = response.preferredLanguage;
      });
  }

  @tracked language = 'en';

  get greeting() {
    switch (this.language) {
      case 'en':
        return 'Hello';
      case 'de':
        return 'Hallo';
      case 'es':
        return 'Hola';
    }
  }
}

This will also trigger a rerender. No matter where the update occurs, updating a tracked property will let Ember know to rerender any affected portion of the app.

Tracking Through Methods

So far we've only shown tracked properties working through getters, but tracking works through methods or functions as well:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class HelloComponent extends Component {
  @tracked language = 'en';
  @tracked supportedLanguages = ['en', 'de', 'es'];

  isSupported(language) {
    return this.supportedLanguages.includes(language);
  }

  get greeting() {
    if (!this.isSupported(this.language)) {
      return 'Unsupported Language';
    }

    switch (this.language) {
      case 'en':
        return 'Hello';
      case 'de':
        return 'Hallo';
      case 'es':
        return 'Hola';
    }
  }
}

if supportedLanguages changes here, greeting will update as well! This code could likely be refactored to use getters, but in cases where a function or method makes more sense, tracked properties will still work.

Tracked Properties in Custom Classes

Tracked properties can also be applied to your own custom classes, and used within your components and routes:

src/utils/person.js
export default class Person {
  @tracked title;
  @tracked name;

  constructor(title, name) {
    this.title = title;
    this.name = name;
  }

  get fullName() {
    return `${this.title} ${this.name}`;
  }
}
app/routes/application.js
import Route from '@ember/routing/route';
import Person from '../../../../utils/person';

export default class ApplicationRoute extends Route {
  model() {
    return new Person('Dr.', 'Zoey');
  }
}
app/controllers/application.js
import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class ApplicationController extends Controller {
  @action
  updateName(title, name) {
    this.model.title = title;
    this.model.name = name;
  }
}
app/templates/application.hbs
{{@model.fullName}}

<button type="button" {{on "click" (fn this.updateName 'Prof.' 'Tomster')}}>
  Update Name
</button>

As long as the properties are tracked, and accessed when rendering the template directly or indirectly, everything should update as expected

Plain Old JavaScript Objects (POJOs)

Generally, you should try to create classes with their tracked properties enumerated and decorated with @tracked, instead of relying on dynamically created POJOs. In some cases however, if your usage of properties on POJOs is too dynamic, you may not be able to enumerate every single property that could be tracked. In this case, you can use TrackedObject from tracked-built-ins:

import { TrackedObject } from 'tracked-built-ins';

let obj = new TrackedObject({
  a: 1,
  b: 2,
})

// This change is tracked
obj.c = 3;

All property reading and writing on this object is automatically tracked. TrackedObject is "shallowly" tracked. obj.c = 4 would be tracked, but obj.c.somethingDeeper = 5 would not be tracked unless you've also made sure that the contents of obj.c is itself another TrackedObject.

Arrays

When you want to track the contents of an Array, you can use TrackedArray from tracked-built-ins:

import { TrackedArray } from 'tracked-built-ins';

class ShoppingList {
  items = new TrackedArray([]);

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

TrackedArray supports all the normal native Array methods, ensuring that their reads and writes are tracked.

Caching of tracked properties

In contrast to computed properties from pre-Octane, tracked properties are not cached. A tracked property can also be recomputed even though its dependencies haven't changed. The following example shows this behavior:

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

let count = 0;

class Photo {
  @tracked width = 600;
  @tracked height = 400;

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

let photo = new Photo();

console.log(photo.aspectRatio); // 1.5
console.log(count); // 1
console.log(photo.aspectRatio); // 1.5
console.log(count); // 2

photo.width = 800;

console.log(photo.aspectRatio); // 2
console.log(count); // 3

From the value of count, we see that aspectRatio was calculated 3 times.

Recomputing is fine in most cases. If the computation that happens in the getter is very expensive, however, you will want to cache the value and retrieve it when the dependencies haven't changed. You want to recompute only if a dependency has been updated.

Ember's @cached decorator lets you cache (or "memoize") a getter by simply marking it as @cached.

With this in mind, let's introduce caching to aspectRatio:

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

let count = 0;

class Photo {
  @tracked width = 600;
  @tracked height = 400;

  @cached
  get aspectRatio() {
    count++;
    return this.width / this.height;
  }
}

let photo = new Photo();

console.log(photo.aspectRatio); // 1.5
console.log(count); // 1
console.log(photo.aspectRatio); // 1.5
console.log(count); // 1

photo.width = 800;

console.log(photo.aspectRatio); // 2
console.log(count); // 2

From the value of count, we see that, this time, aspectRatio was calculated only twice.

In general, you should avoid using @cached unless you have confirmed that the getter you are decorating is computationally expensive, since @cached adds a small amount of overhead to the getter.

The @cached decorator was released in Ember 4.1. If you want to leverage this API between versions 3.13 and 4.1, you can install ember-cached-decorator-polyfill to your project.

left arrow
Customizing Serializers
Patterns for Components
right arrow
On this page

  • Autotracking Basics
  • Updating Tracked Properties
  • Tracking Through Methods
  • Tracked Properties in Custom Classes
  • Plain Old JavaScript Objects (POJOs)
  • Caching of tracked properties
Team Sponsors Security Legal Branding Community Guidelines
Twitter GitHub Discord Mastodon

If you want help you can contact us by email, open an issue, or get realtime help by joining the Ember Discord.

© Copyright 2025 - Tilde Inc.
Ember.js is free, open source and always will be.


Ember is generously supported by
blue Created with Sketch.