Mastering Ember’s @tracked Decorator: From Autotracking to Practical Patterns
Learn how Ember’s @tracked decorator replaces observers, how the dirty‑bit mechanism keeps updates granular, and what common pitfalls to avoid. A hands‑on guide with code, migration steps, and testing tips.
14 Feb 2026, 18:50 UTC

Quick Takeaway
The @tracked decorator is the heart of Ember Octane’s reactivity. Mark a field as @tracked and any get that reads it, or any template that references it, automatically becomes a dependent computation. When the field changes, Ember only invalidates those dependents via a dirty‑bit mechanism—no full component tree sweep, no observers, no manual notifyPropertyChange calls.
How @tracked Works Under the Hood
When a component is rendered, the Glimmer VM records every tracked property that a get or template reads. These become nodes in a dependency graph. Each node carries a dirty bit. On assignment, the VM flips the bit and propagates it up the graph, but only along paths that actually read the property. This fine‑grained invalidation keeps rendering fast even in large apps.
Dependency Graph Example
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class UserProfile extends Component {
@tracked firstName = '';
@tracked lastName = '';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
Template: {{this.fullName}} creates a dependency on fullName, which in turn depends on firstName and lastName. When firstName changes, only fullName and the template fragment that reads it are re‑evaluated.
Updating Tracked State
Inside an action you can mutate tracked fields normally:
this.firstName = 'Alice';
For collections, the decorator only watches the reference, not the contents. Use immutable updates or tracked built‑ins:
this.items = [...this.items, newItem];
or
import { tracked } from '@glimmer/tracking';
tracked(this.items).push(newItem);
Common Pitfalls & Limits
- In‑place mutations don’t trigger updates:
this.items.push(x)leaves the reference unchanged. - Missing
@trackedon a field: Plain classes without the decorator are inert in templates. @trackedon a getter is a no‑op: Track the underlying fields instead.- Prototype changes are ignored: Dynamically added properties after construction aren’t tracked.
- Testing timing:
render()doesn’t auto‑flush tracked changes; useawait settled()orawait nextRender(). - Mixing observers and
@tracked: Legacy observers still fire but may cause double renders.
Avoiding In‑Place Mutations
When you need to modify an array, replace the reference or use tracked built‑ins:
this.list = [...this.list, newItem];
// or
tracked(this.list).push(newItem);
Both approaches flip the reference and set the dirty bit, triggering a re‑render.
Testing with Tracked Changes
Example test snippet:
import { render, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import hbs from 'htmlbars-inline-precompile';
module('Acceptance | tracked', function() {
test('tracked array update re-renders', async function(assert) {
await render(hbs``);
this.set('items', [1, 2]);
await settled();
assert.dom('.item').exists({ count: 2 });
});
});
Use await settled() to wait for the Glimmer VM to process dirty bits.
Migration Path from Computed to @tracked
Legacy computed('foo', 'bar', function() { … }) can be replaced by a getter that reads @tracked fields. The new getter automatically becomes an autotracked computation.
| Old Pattern | New Pattern |
|---|---|
@computed('firstName','lastName') get fullName() | @tracked firstName; @tracked lastName; get fullName() { return `${this.firstName} ${this.lastName}`; } |
Practical Patterns
- Async Derived State:
@tracked user = null; async loadUser(id) { this.user = await this.store.findRecord('user', id); }– templates guard with{{#if this.user}}to wait for resolution. - Service Injection: Inject a service, mark its internal state
@tracked, and any component consuming the service automatically updates. - Deep Reactive Structures: For nested objects, either use immutable updates or wrap with
tracked(this.obj).set('key', value)from RFC 0496.
Verifying Reactions
Use the Ember Inspector:
- Components tab →
Tracked Propertiespanel shows dirty bits and dependency graph. - Console
this.trackedPropertiescan be inspected in component instances.
In tests, assert DOM changes after await settled() to confirm reactivity.
Summary
Ember’s @tracked decorator and autotracking system provide a clean, fine‑grained reactive model that replaces observers and computed properties. By marking only the fields you care about and using immutable patterns for collections, you keep your components fast and maintainable. Remember the pitfalls—especially in‑place mutations and testing timing—and use Ember Inspector to confirm that your dependency graph behaves as expected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.