Native Classes

Native classes are a feature of JavaScript. They are officially supported in Ember Octane for use with:

  • Components (except classic components)
  • Ember Data Models
  • Routes
  • Controllers
  • Services
  • Helpers
  • General utility classes

The ember-native-class-codemod will help you convert your existing code to Native Classes.

For developers who are not already familiar with native classes, check out Ember's native class guide, which provides a thorough breakdown of native class functionality and usage. This section of the upgrade guide will focus on the differences between classic Ember classes and native classes. You can also reference the Octane vs. Classic Cheatsheet as a quick reference for these differences.

Benefits of Native Classes

For existing Ember users, Native Classes might seem a bit strange, but for developers coming from general JavaScript backgrounds or other frameworks, it might be hard for them to imagine Ember any other way.

Before classes were available in JavaScript, Ember developers still got to use some class-like features thanks to @ember/object. Now that classes are available in JavaScript, we can do away with some of the @ember/object quirks.

Native Classes for classic component

The only class that is not supported out of the box is the classic Ember component class, i.e. one imported from @ember/component. However, you can instead use external addons like ember-decorators if you want to convert these to native classes, and refer to their documentation as a guide.

constructor instead of init

When using native classes, you should use constructor instead of the init function:

// Before
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default Controller.extend({
  store: service(),

  init() {
    this._super(...arguments);

    this.featureFlags = this.store.findAll('feature-flag');
  },
});
// After
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default class ApplicationController extends Controller {
  @service store;

  constructor() {
    super(...arguments);

    this.featureFlags = this.store.findAll('feature-flag');
  }
}

The init hook still exists on many existing classes, and runs after constructor, so you can generally convert to native class syntax without rewriting your init methods. However, in the future init will be removed, so you should eventually transition to constructor.

It's important to note that only explicit injections are available during class construction (e.g. injections added using @service). If you still rely on implicit injections, like Ember Data automatically injecting the store service, you will need to add it explicitly instead:

import Controller from '@ember/controller';

export default class ApplicationController extends Controller {
  constructor() {
    super(...arguments);

    this.featureFlags = this.store.findAll('feature-flag');
    // Error: store is undefined, so this will break
  }
}

Adding explicit injections in general is a highly recommended practice.

Fields vs. Properties

Native classes have fields instead of properties:

// Before
import Controller from '@ember/controller';

export default Controller.extend({
  title: 'hello-world.app',
});
// After
import Controller from '@ember/controller';

export default class ApplicationController extends Controller {
  title = 'hello-world.app';
}

Fields are syntactic sugar for assigning the value in the constructor, like so:

import Controller from '@ember/controller';

export default class ApplicationController extends Controller {
  constructor() {
    super(...arguments);
    this.title = 'hello-world.app';
  }
}

This means that the field created is assigned for every instance, instead of once on the prototype like properties. This has a few important implications:

  1. It is now safe to assign objects to fields! You can assign an array or an object to your field, and it won't be shared between instances of the class:

    import Component from '@glimmer/component';
    
    export default class ShoppingListComponent extends Component {
     // This is completely ok!
     items = ['milk', 'potatoes'];
    }
    
  2. Performance can be a concern with fields, since they eagerly create new values for every instance of the component. This is generally not a problem, but is something to be aware of.

  3. If you are mixing native and classic class definitions, then class fields from a parent class can override class properties:

    import Controller from '@ember/controller';
    
    class BaseController extends Controller {
     title = 'default';
    }
    
    export default BaseController.extend({
     // this title property will be overridden by the
     // class field in the parent class
     title: 'My Title',
    });
    

Other than that, fields can generally safely replace properties.

Getters and Setters

Getters and setters can be defined directly on native classes:

export default class Image {
  width = 0;
  height = 0;

  get aspectRatio() {
    return this.width / this.height;
  }
}

It's important to note that these are not the same as computed properties, they don't have caching by default or have dependencies, and they rerun every time they are used. In order to have getters and setters rerender when values have changed, you must either decorate them with the @computed decorator, or use tracked properties.

Classic classes didn't have an equivalent for native getters and setters until recently, but you can define them now with the standard JavaScript getter syntax:

export default EmberObject.extend({
  width: 0,
  height: 0,

  get aspectRatio() {
    return this.width / this.height;
  },
});

Decorators

Decorators are a new concept in JavaScript, but if you've never seen them before, don't worry, they've been used in Ember for years. computed() is in fact a type of decorator:

import EmberObject, { computed } from '@ember/object';

export default EmberObject.extend({
  width: 0,
  height: 0,

  aspectRatio: computed('width', 'height', {
    get() {
      return this.width / this.height;
    },
  }),
});

The native decorator version functions the same, just with a slightly different syntax:

import { computed } from '@ember/object';

export default class Image {
  width = 0;
  height = 0;

  @computed('width', 'height')
  get aspectRatio() {
    return this.width / this.height;
  }
}

Notice that you don't need to pass in the get function to the decorator itself. Instead, the decorator gets applied to the getter function, modifying it in place. Existing computed properties and computed property macros, including custom ones you've defined, can be used with this new syntax:

import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';

function join(...keys) {
  return computed(...keys, {
    get() {
      return keys.map(key => this[key]).join(' ');
    },
  });
}

// Before
const ClassicPerson = EmberObject.extend({
  nickname: 'Tom',
  title: 'Prof.',
  name: 'Tomster',

  fullName: join('title', 'name'),
  displayName: alias('nickname'),
});

// After
class Person {
  nickName = 'Tom';
  title = 'Prof.';
  name = 'Tomster';

  @join('title', 'name') fullName;
  @alias('nickname') displayName;
}

Other decorators exist, including @tracked which will be discussed later on, and the @action decorator. The @action decorator replaces the actions hash on routes, controllers, and components:

// Before
import Controller from '@ember/controller';

export default Controller.extend({
  actions: {
    helloWorld() {
      console.log('Hello, world!');
    },
  },
});
// After
import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class ApplicationController extends Controller {
  @action
  helloWorld() {
    console.log('Hello, world!');
  }
}

The action decorator also binds actions, so you can refer to them directly in templates without the {{action}} helper:

{{!-- Before --}}
<OtherComponentHere @update={{action 'helloWorld'}} />
{{!-- After --}}
<OtherComponentHere @update={{this.helloWorld}} />

super

In native classes, there is a dedicated super keyword that replaces the _super() method:

// Before
const Person = EmberObject.extend();

const Firefighter = Person.extend({
  init() {
    this._super(...arguments);
    this.name = 'Rob Jackson';
  },

  saveKitten() {
    this._super(...arguments);
    console.log('kitten saved!');
  }
});

// After
class Person {}

class Firefighter extends Person {
  constructor() {
    super();
    this.name = 'Rob Jackson';
  }

  saveKitten() {
    if (super.saveKitten) {
      super.saveKitten(...arguments);
    }

    console.log('kitten saved!');
  }
}

As you can see, it functions a little bit differently that the _super() method. When used in a constructor, you call it directly like a function. You must do this before using this in the constructor, otherwise it's a syntax error. However, when used in any other method, you must explicitly specify the function you are calling on the parent class.

Another difference is that unlike _super(), if the method doesn't exist on the parent class then an error will be thrown. In most cases, the method should exist or not, and you shouldn't need to guard it one way or the other.

static

In classic classes, if you wanted to add values to the class itself, you had to use the reopenClass method:

const Vehicle = EmberObject.extend({
  init() {
    this._super();
    this.id = Vehicle.count;
    Vehicle.incrementCount();
  },
});

Vehicle.reopenClass({
  count: 0,
  incrementCount() {
    this.count++;
  },
});

In native classes this can be done with the static keyword instead:

class Vehicle {
  static count = 0;
  static incrementCount() {
    this.count++;
  }

  constructor() {
    this.id = Vehicle.count;
    Vehicle.incrementCount();
  }
}

The static keyword can be applied to all class elements.

Mixins

Native class syntax does not directly have an equivalent for the Ember mixin system. If you want to continue using mixins as you convert, you can do so by mixing classic class extension syntax with native class syntax:

export default class Vehicle extends EmberObject.extend(MotorMixin) {
  // ...
}

In addition, some new framework classes, such as Glimmer components, do not support Ember mixins at all. In the future, mixins will be removed from the framework, and will not be replaced directly. For apps that use mixins, the recommended path is to refactor the mixins to other patterns, including:

  • Pure native classes, sharing functionality via class inheritance.
  • Utility functions which can be imported and used in multiple classes.
  • Services which can be injected into multiple classes, sharing functionality and state between them.

Cheatsheet

This cheatsheet is a quick reference for the best practices and differences in native and classic classes. Remember, you should prefer using native class syntax and not extending from EmberObject at all in your apps.

Definitions

Native

  • Use class when defining a class, and class ... extends when extending a class.

    class Person {}
    
    class Actress extends Person {}
    
  • Always give your class a name, e.g. ✅ class MyClass {} and not ???? class {}

Classic

  • Use the extend static method to define a class, with EmberObject as the root base class.

    const Person = EmberObject.extend({});
    
    const Actress = Person.extend({});
    

Instantiation

Native

  • Use the new keyword to create instances of the class

    class Person {}
    
    let jen = new Person();
    
  • Arguments passed when using new will be accessible in the constructor of the class:

    class Person {
    constructor(name) {
      this.name = name;
    }
    }
    
    let jen = new Person('Jen Weber');
    console.log(jen.name); // Jen Weber
    
  • Prefer the constructor function, unless the class extends EmberObject, in which case prefer init.

Classic

  • Use the create static method to create instances of the class:

    const Person = EmberObject.extend({});
    
    let jen = Person.create();
    
  • You can pass an object of values to create, and they'll be assigned to the instance:

    const Person = EmberObject.extend({});
    
    let jen = Person.create({ name: 'Jen Weber' });
    console.log(jen.name); // Jen Weber
    
  • Use the init method instead of the constructor.

Methods

Mostly the same between native and classic:

Native

class Person {
  helloWorld() {
    console.log('Hello, world!');
  }
}

Classic

const Person = EmberObject.extend({
  helloWorld() {
    console.log('Hello, world!');
  },
});

Properties and Fields

Native

  • Native classes have fields. Fields are created and assigned for every instance:

    class Person {
    name = 'Chad Hietala';
    }
    
  • It is okay to assign objects and arrays in class fields:

    // ok ✅
    class Person {
    shoppingList = [];
    }
    
  • Avoid using class state in field definitions, use the constructor instead:

    // bad ????
    class Image {
    width = 0;
    height = 0;
    
    aspectRatio = this.width / this.height;
    }
    
    // good ✅
    class Image {
    constructor() {
      this.aspectRatio = this.width / this.height;
    }
    
    width = 0;
    height = 0;
    }
    
  • Fields are assigned before any constructor code is run, so you can access their values in the constructor function.

Classic

  • Classic classes have properties. Properties are created and assigned once to the prototype of the class, and are shared between every instance:

    const Person = EmberObject.extend({
    name: 'Chad Hietala',
    });
    
  • It is not okay to assign objects or arrays as properties, because they are shared between instances:

    // not ok ????
    const Person = EmberObject.extend({
    shoppingList: [],
    });
    

Accessors

These are also mostly the same between native and classic classes.

  • Accessors can be defined with the get and set keywords:

    class Person {
    _name = 'Mel Sumner';
    
    get name() {
      return this._name;
    }
    
    set name(newName) {
      this._name = newName;
    }
    }
    
  • Getters run every time the property is read, setters run every time the property is set.

  • Getters should not mutate state, and should be idempotent (they return the same value every time if nothing else has changed).

Decorators

Native

  • Decorators are modifiers that change the behavior of a field, method, or class.

  • Native decorators are functions that get applied using the @ symbol:

    import { tracked } from '@glimmer/tracking';
    
    class Person {
    @tracked name = 'Ed Faulkner';
    }
    
  • Native decorators can be applied to class fields, methods, accessors, or classes themselves. Generally, specific decorators are only meant to be applied to one or two of these types of things.

  • Decorators can also receive arguments, and some decorators must receive them.

  • Every decorator is unique! See the documentation for each decorator to see how it should be used.

Classic

  • Classic decorators are assigned like properties in classic class definitions:

    import EmberObject from '@ember/object';
    import { tracked } from '@glimmer/tracking';
    
    const Person = EmberObject.extend({
    name: tracked({ value: 'Ed Faulkner' }),
    });
    
  • Only specific decorators provided by Ember can be applied this way in classic classes.

Static Elements

Native

  • Adding the static keyword to a class element definition puts it on the class itself, instead of instances:

    class Person {
    static name = 'Ed Faulkner';
    }
    
    console.log(Person.name); // Ed Faulkner
    
    let person = new Person();
    
    console.log(person.name); // undefined
    

Classic

  • Use reopenClass to add static elements to the constructor:

    const Person = EmberObject.extend();
    
    Person.reopenClass({
    name: 'Ed Faulkner',
    });
    

Super

Native

  • Use the super keyword

  • In constructors, use the keyword by itself (this is required). Generally pass any arguments along as well:

    class TodoComponent extends Component {
    constructor() {
      super(...arguments);
    
      // setup the component...
    }
    }
    
  • In all other cases, specify the method you want to call when using super:

    class Vehicle {
    moveType = 'moving';
    
    move() {
      console.log(`${this.moveType}!`);
    }
    }
    
    class Aircraft extends Vehicle {
    moveType = 'flying';
    
    fly() {
      super.move();
    }
    }
    
    let airbus = new Aircraft();
    airbus.fly(); // flying!
    
  • If the method does not exist on the parent class, it will throw an error.

Classic

  • Use the _super() function to call the super method with the same name as the current method that is executing:

    const Vehicle = EmberObject.extend({
    move() {
      console.log(`moving!`);
    },
    });
    
    const Aircraft = Vehicle.extend({
    move() {
      this._super();
      console.log('flying!');
    },
    });
    
    let airbus = new Aircraft();
    airbus.move(); // moving! flying!
    
  • Calling _super() is required for init to function properly. It should generally be done before you do anything else in init.

  • It will not error if the method does not exist on the parent class.

Extending Classic Classes with Native Syntax

  • It is possible to extend classic classes with native syntax, and to toggle back and forth between the two:

    class Vehicle extends EmberObject {
    move() {
      // ...
    }
    }
    
    const Aircraft = Vehicle.extend({
    fly() {
      // ...
    },
    });
    
    class Helicopter extends Aircraft {
    hover() {
      // ...
    }
    }
    
    let blackHawk = Helicopter.create();
    
  • Use init instead of constructor

  • Use create instead of new

  • Otherwise, when using native class syntax, native class rules and behaviors apply, and when using classic class syntax, classic class rules apply.

© 2020 Yehuda Katz, Tom Dale and Ember.js contributors
Licensed under the MIT License.
https://guides.emberjs.com/v3.25.0/upgrading/current-edition/native-classes