Using @tracked in Ember Octane for Reactive UI Without Observers
Learn how Ember Octane's @tracked decorator drives automatic template updates, see a working counter example, and understand the limits of deep property changes.
08 Sept 2026, 12:30 UTC

Why UI Updates Can Feel Unreliable
In a Glimmer component you might store a value in a plain JavaScript object and expect the template to refresh when you change a nested field. After clicking a button that increments state.count you see the number stay the same, even though the underlying data changed. The problem is that Ember’s reactivity only watches property accesses that happen during rendering or inside tracked getters; a plain property change that isn’t read in those contexts is ignored.
How @tracked Enables Autotracking
Ember Octane introduces the @tracked decorator for native class fields. When a field is marked @tracked, Ember records every time that field is read while a template is rendering or a tracked getter is executing. If the field later changes, Ember re‑runs only the parts of the template that depended on it. This eliminates the need for manual notifyPropertyChange calls or classic observers.
Worked Example: A Counter with a Derived Value
Create a Glimmer component app/components/counter.js:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class CounterComponent extends Component {
@tracked count = 0;
get doubleCount() {
// This getter is tracked because it reads @tracked count
return this.count * 2;
}
@action
increment() {
this.count += 1;
}
}
The template app/components/counter.hbs uses both values:
<button {{on 'click' this.increment}}>Increment</button>Count: {{this.count}}
Double: {{this.doubleCount}}
When you click the button, Ember records that the
incrementaction writes a new value tocount. Becausecountis tracked, the template sections that readcountanddoubleCountare scheduled for a re‑render. You can verify this by opening the component in a local Ember serve, clicking the button, and observing both numbers update without adding any observer code.Limitation: Deep Mutations Are Not Tracked
If you replace the plain field with an object,
@tracked state = { count: 0 }, then mutatingthis.state.count = 1does not trigger an update. The tracker only sees the read ofstateitself, not the nested property. To make nested changes reactive you have two options:
- Replace the whole object immutably:
this.state = { ...this.state, count: this.state.count + 1 }. - Mark the nested field as tracked on a separate class or use
@trackedon a service that holds the object.
These approaches ensure that the property read during rendering is the one that actually changes.
Actionable Steps to Verify @tracked in Your App
- Run
ember --versionand confirm you are on Ember 3.25 or later (Octane stable). - Add a simple component as shown above, start
ember serve, and click the button. - Open the browser console and add a temporary
console.loginside thedoubleCountgetter; you should see the log only when the count changes, not on unrelated interactions. - If you need to track nested state, try the immutable update pattern and confirm the UI updates.
By following these steps you can be confident that @tracked is driving the reactivity you expect, and you know when to switch to immutable updates for deeper data structures.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.