Migrating from Computed Properties to @tracked in Ember
Learn how to replace legacy computed properties and observers with Ember's @tracked system to eliminate dependency arrays and improve application performance.
27 Jul 2026, 08:52 UTC

The Problem: Dependency Hell and Boilerplate
Legacy Ember applications rely on computed() properties and observers to manage state. These patterns require explicit dependency arrays (dependentKeys), which are prone to human error: if you add a new variable to a computed property but forget to update the dependency list, the UI fails to update. Observers further complicate this by introducing unpredictable execution orders and potential infinite loops.
The takeaway: Autotracking removes the need for dependency arrays. By marking a property with @tracked, Ember automatically detects which templates or getters use that property and updates them only when the value changes.
Prerequisites
- Ember Version: 3.15+ (Octane). Native class syntax (3.20+) is strongly recommended.
- Build Pipeline: Ensure
@babel/plugin-proposal-decoratorsis configured in yourember-cli-babelsettings to support the@syntax. - Class Structure: This guide assumes you are using native JavaScript classes (
export default class Component extends Component) rather thanEmberObject.extend().
Converting State and Derived Data
1. Replace Computed Properties with Getters
In the legacy system, a computed property explicitly watched its dependencies. In the autotracking system, you use a native JavaScript getter. Ember tracks which @tracked properties are accessed during the execution of that getter.
Legacy Pattern:
// Old style
fullName: computed("firstName", "lastName", function() {
return `${this.firstName} ${this.lastName}`;
})
Modern Pattern:
import { tracked } from "@glimmer/tracking";
export default class UserProfile extends Component {
@tracked firstName = "Jane";
@tracked lastName = "Doe";
get fullName() {
// Ember automatically tracks firstName and lastName here
return `${this.firstName} ${this.lastName}`;
}
}
2. Replacing Observers with Side-Effect Setters
Observers are discouraged because they fire unpredictably. Instead, move the logic into the action or setter that changes the state.
Instead of an observer:
// AVOID THIS
observers: {
userNameChanged() {
this.saveToDatabase();
}
}
Use an action:
@action
updateName(newName) {
this.userName = newName;
this.saveToDatabase(); // Explicit side-effect
}
Handling Collections and Objects
Autotracking works on the reference of the property. If you mutate a property inside an object that is not itself tracked, Ember will not detect the change.
| Scenario | Behavior | Solution |
|---|---|---|
| Primitive (String/Number) | Updates on assignment | Use @tracked |
| Array Mutation (push/pop) | Detected if array is tracked | Use @tracked or replace array reference |
| Nested Object Property | Not detected | Replace the whole object or use a tracked class |
Example of the Nested Object Trap:
@tracked user = { name: "Jane" };
// This will NOT trigger a re-render:
this.user.name = "Janet";
// This WILL trigger a re-render:
this.user = { ...this.user, name: "Janet" };
Verification and Testing
To verify that your migration is working, you must ensure the UI reacts to state changes without explicit notifyPropertyChange() calls.
Manual Check
- Create a component with a
@trackedcounter and a getter that doubles that counter. - Trigger an action to increment the counter.
- Verify the doubled value in the template updates instantly.
Automated Testing
When writing rendering tests, use settled() after mutating tracked properties to ensure the Glimmer VM has flushed the validator tags and updated the DOM.
// In a rendering test
await render(MyComponent);
await this.find('button').click();
await settled(); // Wait for autotracking to trigger DOM update
assert.dom('.result').hasText('Expected Value');
Limitations and Risks
- Memory Overhead: While autotracking is generally faster than the old dependency graph, creating thousands of individual
@trackedproperties on a single large object can increase validation overhead. For massive datasets, prefer replacing the entire object reference. - FastBoot Serialization: If using Server-Side Rendering (FastBoot), ensure your tracked properties contain serializable data. Tracking class instances or DOM nodes can cause hydration errors.
- Re-entrancy: Because tracked updates can trigger synchronously in some contexts, avoid mutating a tracked property inside a getter that depends on that same property, as this can cause an infinite loop.
Rollback Strategy
Since this migration involves changing class property definitions, the primary rollback is a git revert of the affected component/service. If you are performing an incremental migration, @tracked properties can coexist with legacy computed() properties in the same class, allowing you to revert individual properties without breaking the entire component.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.