Ember Autotracking: Why Your Template Stops Updating on Push
Autotracking in Ember records what a template reads, not how you mutate data. Learn why push on a plain array is silent, how @tracked getters replace computed properties, and when to reassign or use tracked-built-ins.
01 Jul 2026, 20:24 UTC

You add an item to a cart array with this.items.push(item) and the total in the template stays the same. The value is correct in memory, the action ran, but the UI does not move. That is the classic autotracking gotcha in Ember Octane.
The useful takeaway: Ember's autotracking records what a template and derived getters read during render, not how you mutate data. If you change a value without creating a new reference for a tracked property, nothing is invalidated.
Autotracking replaces dependency lists with plain JavaScript reads
Introduced with the Octane edition in Ember 3.15+, autotracking lets native class fields marked with @tracked and plain getters participate in reactivity. A tracked property is only observed when it is consumed.
During render, Ember walks the template and any derived getters. Each read of a tracked value is recorded as a dependency for that render. Later, when the tracked value is set to a new value, Ember knows exactly which consumers to revalidate. Multiple synchronous sets are coalesced into a single revalidation by Ember's render scheduler, which avoids thrash without requiring manual batching.
This is a shift from the pre-Octane model of computed properties with explicit dependent keys, observers, and Ember.set. Autotracking removes the dependency list and the boilerplate, but it requires Octane-era idioms: native classes, Glimmer components, and decorators where your edition enables them.
Worked example: a cart total without computed properties
A minimal Glimmer component shows the mental model.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class CartComponent extends Component {
@tracked items = [];
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0);
}
add(item) {
// Correct: new reference triggers autotracking
this.items = [...this.items, item];
}
}
Template:
<ul>
{{#each this.items as |item|}}
<li>{{item.name}} - ${{item.price}}</li>
{{/each}}
</ul>
Total: ${{this.total}}
<button type="button" {{on "click" (fn this.add (hash name="Sock" price=5))}}>Add</button>
Here items is tracked. The total getter reads this.items. When the template renders, it consumes both. Setting this.items to a new array invalidates the consumption, so the each block and the total getter re-run. No observers, no Ember.computed, no manual dependency array.
The limitation: in-place mutation is silent
Autotracking tracks assignments to tracked properties, not mutations inside them. A plain array or object is only tracked by reference.
// Does NOT trigger update
this.items.push(item);
// Does trigger update
this.items = [...this.items, item];
This is the most common source of "why didn't my template update?" bugs. The same applies to nested objects: changing this.user.name will not notify unless user itself is reassigned or the nested field is also tracked.
For collections that need mutation methods to be reactive, the community addon tracked-built-ins provides TrackedArray, TrackedMap, and TrackedSet. These are not core Ember APIs. They wrap native structures so that push, splice, and set create the necessary notifications. Use them consciously, as they add a dependency and change semantics from plain JavaScript.
Computed properties and observers still exist for legacy code. An app mid-migration can mix them, but new code benefits from the smaller surface area of tracked state plus getters.
Check it in your app
- Create a component with a
@trackedfield and a derived getter that reads it. Confirm the template updates on reassignment. - Intentionally mutate a plain array in place and observe that the template does not update. Reassigning the array restores updates.
- If you need mutable collections, add
tracked-built-insto a scratch app and compareTrackedArraybehavior against a plain array. - Review your edition defaults and lint rules for decorator syntax. Exact defaults vary by Ember CLI version and whether you use native classes with decorators or plain-class syntax.
Autotracking makes Ember feel closer to plain JavaScript: mark state with @tracked, derive with getters, and let the framework invalidate only what was read. The price is discipline around references. When you replace a reference instead of mutating in place, updates become predictable and fine-grained.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.