Why Ember Octane’s @tracked Autotracking Makes UI Updates Predictable
Learn how replacing classic Ember computed properties with @tracked fields and autotracking gives fine‑grained reactivity, reduces boilerplate, and avoids stale UI.
19 Dec 2025, 04:44 UTC

The problem: stale UI and hidden dependencies
In classic Ember components built with Ember.Object.extend, state lives in plain properties and computed properties declare their dependencies implicitly. When you mutate a nested object—for example, this.todos[0].title = 'new'—the framework does not automatically know that a template reading todos.0.title changed unless you added an observer or a computed property that explicitly listed that path. The result is either stale UI or a proliferation of observers that fire on every change, making the data flow hard to reason about.
Thesis: @tracked + autotracking gives explicit, fine‑grained reactivity
Ember Octane introduced native class components and the @tracked decorator. Autotracking records which tracked properties are accessed during template rendering. When a tracked property later changes, only the parts of the template that read it are updated. This makes reactivity explicit: you mark the mutable state, and the framework derives the update graph automatically, removing the need for observers and reducing boilerplate.
How autotracking works
During render, Ember wraps each tracked property access in a getter that notes the current Tracking context. If the property is later mutated, the tracker notifies all dependent consumers. Because tracking is shallow, only the reference itself is watched; mutating a nested property requires reassigning the tracked reference to trigger an update.
Migrating a classic component to Octane syntax
Consider a simple todo‑item component. In classic form:
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
title: '',
isDone: false,
displayTitle: computed('title', function() {
return this.title.trim();
})
});
The Octane version uses a native class and @tracked:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class TodoItem extends Component {
@tracked title = '';
@tracked isDone = false;
get displayTitle() {
return this.title.trim();
}
}
The getter displayTitle is now a plain getter; any template that uses {{displayTitle}} will automatically rerender when title changes, without declaring a dependency.
Worked example: a counter with a tracked array
Imagine a component that shows a list of numbers and lets the user increment each item. The component state includes a tracked array values and a derived getter sum.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class NumberList extends Component {
@tracked values = [1, 2, 3];
get sum() {
return this.values.reduce((a, b) => a + b, 0);
}
increment(index) {
// Shallow tracking: we must replace the array reference
this.values = [...this.values];
this.values[index] += 1;
}
}
In the template:
<ul>
{{#each this.values as |val i|}}
<li>
{{val}} +1
</li>
{{/each}}
</ul>
<p>Sum: {{this.sum}}</p>
When the user clicks a button, increment creates a new array, mutates the nested element, and reassigns this.values. Because values is tracked, the {{#each}} block and the sum getter both rerender, updating the UI without any observers or computed property dependencies.
What happens if you forget to reassign?
If you instead wrote this.values[index] += 1 without reassigning this.values, the template would not update. Autotracking only notices a change to the tracked reference itself; mutating the array’s contents is invisible to the tracking system. This shallow‑tracking limitation is a common pitfall during migration.
Trade‑offs and limitations
- Shallow tracking: Nested object mutations require reassignment of the tracked reference. Developers must adopt immutable‑style updates or explicit reassignment.
- Explicit discipline: Every piece of mutable state that influences the UI must be decorated with
@tracked. Forgetting a decorator leads to silent stale UI, though the error is usually noticeable during testing. - Learning curve: Teams accustomed to observers or classic computed properties need to adjust to thinking in terms of tracked property accesses during render.
These trade‑offs are offset by the removal of observer boilerplate, clearer data flow, and the ability to reason about updates locally.
Actionable steps for adoption
- Audit existing components: Look for mutable fields, observers, and computed properties that depend on nested properties.
- Convert to native classes: Replace
Ember.Component.extendwithimport Component from '@glimmer/component'and use class syntax. - Add
@tracked: Mark all mutable properties that affect the template. - Remove observers and computed properties: Replace them with plain getters that derive UI state from tracked fields.
- Verify updates: Change a tracked property and confirm only the relevant template sections rerender (Ember Inspector’s
renderingstab can help).
By following these steps, you gain predictable UI updates with less boilerplate and a clearer mental model of how data flows through your Ember application.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.