Initializing Class Fields in Ember Octane

One of the many small-but-lovely benefits of getting to use native classes in Ember Octane.

Assumed audience: Software developers working with Ember Octane.

Long-time Ember.js developers have often internalized the idea that any initialization should happen explicitly in Ember classic classes’ init hook, and not directly in the plain old JavaScript object (POJO) which defines the classic class.

Bad classic code:

import Component from '@ember/component';

export default Component.extend({
  someData: [],
})

Good classic code:

import Component from '@ember/component';
import { set } from '@ember/object';

export default Component.extend({
  init() {
    this._super(...arguments);
    set(this, 'someData', []);
  }
})

This is because of how classic classes worked: the values passed into the POJO were set on the prototype for the class, and so were shared between all instances of the class. That meant that when you passed an array literal as part of the POJO, like someData: [], that same array would be shared between all instances of the component. This is almost never what you want, so developers learned (and Ember’s official lint rule set enforced!) that any reference types like objects or arrays be set in init.

When working with native classes, though, class fields are not set on the prototype, but on the instance. That means that you can just define values directly as class fields, because this — 

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  someData = [];
}

 — is the same as writing this:

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  someData;
  
  constructor() {
    super(...arguments);

    Object.defineProperty(this, 'someData', {
      enumerable: true,
      configurable: true,
      writable: true,
      value: [],
    });
  }
}

Here I’ve switched to the @glimmer/component base class to use constructor instead of init, but the idea is the same!

For day-to-day purposes, that Object.defineProperty call is basically the same as just doing the assignment in the constructor.1 The net of this is that you can leave behind your long-standing habits of doing assignment in the constructor where you’re just setting up a default value for a class field. The only time you should prefer to do things in the constructor is when it depends on the other state of the class — in other words, when it references this in some way. If it doesn’t refer to this, though, even things like instantiating utility classes can just happen in class field assignment:

import Component from '@glimmer/component';
import Formatter from '../utils/formatter';

export default class MyComponent extends Component {
  formatter = new Formatter();
}

Just one of the many niceties that come from upgrading to Ember Octane!


Notes

  1. The meaningful differences between assignment and defineProperty only come up when you’re stomping a property via inheritance. That’s nearly always a bad idea anyway! ↩︎