Ember Octane — Modern Ember with Tracked Properties and Glimmer
In this tutorial, you will learn about Ember Octane. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember Octane is the modern edition of Ember.js introduced in version 3.15. It brings native JavaScript classes, tracked properties, Glimmer components, and a simplified programming model that reduces boilerplate and improves performance.
What You'll Learn
You will learn the key Octane features: @tracked for reactivity, Glimmer components with args, @action decorator, native classes, and how to migrate from classic Ember patterns.
Why It Matters
Octane represents a fundamental shift in how Ember applications are built. It reduces the mental overhead of computed properties, Observer, and this.get(). Modern Octane code is cleaner, faster, and more approachable.
Real-World Use
An Ember application started in 2019 on classic Ember. After migrating to Octane, the codebase shrank by 30%. Getters replaced computed properties. Tracked properties replaced observers. The Migration took 2 months and eliminated dozens of subtle reactivity bugs.
flowchart LR
subgraph Classic Ember
A[computed()] --> B[this.get()]
B --> C[observer]
C --> D[this.set()]
end
subgraph Octane
E[@tracked] --> F[native getters]
F --> G[@action]
G --> H[direct assignment]
end
Native Classes
Octane uses native ES classes instead of EmberObject.extend().
// Classic Ember
const Person = EmberObject.extend({
name: null,
greet() {
return `Hello, ${this.name}`;
}
});
// Octane — native class
export default class Person {
name = null;
greet() {
return `Hello, ${this.name}`;
}
}
Tracked Properties
@tracked replaces this.set() and computed properties. Assigning a new value triggers re-render.
// app/components/temperature.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class TemperatureComponent extends Component {
@tracked celsius = 0;
@tracked history = [];
get fahrenheit() {
return (this.celsius * 9/5) + 32;
}
get isFreezing() {
return this.celsius <= 0;
}
get isBoiling() {
return this.celsius >= 100;
}
get temperatureClass() {
if (this.isFreezing) return 'cold';
if (this.isBoiling) return 'hot';
return 'moderate';
}
@action
increase() {
this.celsius += 5;
this.history = [...this.history, `Increased to ${this.celsius}C`];
}
@action
decrease() {
this.celsius -= 5;
this.history = [...this.history, `Decreased to ${this.celsius}C`];
}
@action
reset() {
this.celsius = 0;
this.history = [];
}
}
Glimmer Components
Glimmer components are the new standard. They use @args for arguments and have no wrapper element.
// app/components/user-card.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class UserCardComponent extends Component {
get displayName() {
return this.args.user.name || this.args.user.email;
}
get isAdmin() {
return this.args.user.role === 'admin';
}
@action
selectUser() {
this.args.onSelect?.(this.args.user);
}
}
{{! app/components/user-card.hbs }}
{{! No wrapper div — Glimmer components have no outer element }}
<article class="user-card {{if this.isAdmin 'admin'}}" data-test-user>
<img src={{@user.avatar}} alt="" loading="lazy" />
<h3>{{this.displayName}}</h3>
<p>{{@user.email}}</p>
{{#if this.isAdmin}}
<span class="badge admin">Admin</span>
{{/if}}
<button type="button" {{on "click" this.selectUser}}>
View Profile
</button>
</article>
Getters Instead of Computed
Native getters replace computed().
// Classic computed property
fullName: computed('firstName', 'lastName', function() {
return `${this.firstName} ${this.lastName}`;
})
// Octane getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
Getters automatically recompute when any tracked property they access changes.
The @action Decorator
@action binds the method to the component instance.
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class MyComponent extends Component {
// Without @action, this would lose context when passed as callback
@action
handleClick(event) {
// `this` is always correct
console.log('Clicked:', event.target);
}
}
Modifiers Instead of Lifecycle Hooks
Octane uses {{did-insert}}, {{did-update}}, and {{will-destroy}} modifiers.
// app/components/chart.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class ChartComponent extends Component {
chartInstance = null;
@action
initChart(element) {
this.chartInstance = new Chart(element, {
type: 'bar',
data: this.args.data
});
}
@action
updateChart(element, [data]) {
if (this.chartInstance) {
this.chartInstance.data = data;
this.chartInstance.update();
}
}
@action
destroyChart() {
if (this.chartInstance) {
this.chartInstance.destroy();
this.chartInstance = null;
}
}
}
<div
{{did-insert this.initChart}}
{{did-update this.updateChart @data}}
{{will-destroy this.destroyChart}}
>
</div>
No More this.get() and this.set()
Octane allows direct property access.
// Classic
this.get('name'); // Reading
this.set('name', 'Bob'); // Writing
// Octane
this.name; // Reading
this.name = 'Bob'; // Writing (only works with @tracked)
Template Improvements
Octane templates use this. for component properties and @ for arguments.
{{! Classic }}
{{name}}
<MyComponent @name={{name}} />
{{! Octane }}
{{this.name}}
<MyComponent @name={{this.name}} />
Migration Example
// Classic Ember Component
export default Ember.Component.extend({
tagName: 'div',
classNames: ['counter'],
count: 0,
didInsertElement() {
this._super();
console.log('Mounted');
},
click() {
this.incrementProperty('count');
},
countStr: computed('count', function() {
return `Count: ${this.count}`;
})
});
// Octane version
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class CounterComponent extends Component {
@tracked count = 0;
@action
increment() {
this.count++;
}
get countStr() {
return `Count: ${this.count}`;
}
}
{{! Octane template }}
<div class="counter">
<p>{{this.countStr}}</p>
<button type="button" {{on "click" this.increment}}>+1</button>
</div>
Common Mistakes
- Mutating arrays and objects instead of replacing them.
@trackedtracks assignment, not mutation. Usethis.items = [...this.items, item]instead ofthis.items.push(item). - Using
EmberObject.extend()in new code. All new code should use native classes. Classic patterns are only for maintaining legacy code. - Forgetting
this.in templates. Octane templates requirethis.for component properties. Without it, the property is not found. - Not migrating computed properties that depend on non-tracked data. Getters only recompute when tracked dependencies change. Ensure all dependencies are tracked.
- Using
@actionon non-method properties.@actionis for methods. Do not use it on getters or plain properties.
Practice Questions
- What does
@trackeddo? - How are Glimmer components different from classic components?
- What replaces
computed()in Octane? - Why do you need
@actionon event handler methods? - Challenge: Take a classic Ember component that uses
computed(),this.get(),this.set(),EmberObject.extend(), anddidInsertElement. Rewrite it using Octane patterns: native class,@tracked, getters,{{on}}, and{{did-insert}}.
FAQ
Mini Project
Take the product listing application you built earlier. Rewrite it using Octane patterns: (1) Convert all components to Glimmer components with @tracked and @action. (2) Replace all computed() with native getters. (3) Replace {{action}} helper with {{on}} modifier. (4) Replace lifecycle hooks with modifiers. (5) Update templates to use this. syntax.
What's Next
Now that you understand Octane, build a complete application in Ember Project. Then compare with Aurelia for another modern framework approach.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro