Skip to content

Ember Octane — Modern Ember with Tracked Properties and Glimmer

DodaTech Updated 2026-06-28 5 min read

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

  1. Mutating arrays and objects instead of replacing them. @tracked tracks assignment, not mutation. Use this.items = [...this.items, item] instead of this.items.push(item).
  2. Using EmberObject.extend() in new code. All new code should use native classes. Classic patterns are only for maintaining legacy code.
  3. Forgetting this. in templates. Octane templates require this. for component properties. Without it, the property is not found.
  4. Not migrating computed properties that depend on non-tracked data. Getters only recompute when tracked dependencies change. Ensure all dependencies are tracked.
  5. Using @action on non-method properties. @action is for methods. Do not use it on getters or plain properties.

Practice Questions

  1. What does @tracked do?
  2. How are Glimmer components different from classic components?
  3. What replaces computed() in Octane?
  4. Why do you need @action on event handler methods?
  5. Challenge: Take a classic Ember component that uses computed(), this.get(), this.set(), EmberObject.extend(), and didInsertElement. Rewrite it using Octane patterns: native class, @tracked, getters, {{on}}, and {{did-insert}}.

FAQ

Is Ember Octane the latest version?

Octane was introduced in 3.15. Current Ember 5.x continues Octane patterns.

Can I mix classic and Octane components?

Yes, they can coexist. Migrate incrementally.

Do I need to migrate all computed properties?

Not immediately. Classic computed properties still work.

Does Octane improve performance?

Yes. Glimmer VM v2 and tracked properties reduce rendering overhead.

How do I handle observers in Octane?

Use @tracked with getters. Observers are deprecated.

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