Readers' Questions - "When will we be able to use decorators in Ember apps?"


#1

Hello once again to Readers’ Questions, presented by the Ember.js Times.

Today we have a question asked by jj:

When and how will we be able to use decorators in Ember apps?

The short answer is you can actually use decorators with Ember right now!

Last year we merged an RFC that locked in the behavior of using native class syntax with EmberObject, which was a precursor to being able to stably use experimental technologies like decorators and class fields. Since then, the ember-decorators project has been continuing to refine a set of decorators which will hopefully one day actually become part of Ember core (following the path laid out by ember-native-dom-helpers for testing out and refining new expansions to the Ember API).

You can check out this blog post for more details on using decorators along with native ES class syntax in Ember today, and you can see the latest status on the implementation of the ES Class RFC on the Ember Status Board.

Should I use decorators in my app/addon?

Now, just because you can use experimental stage 2/3 technologies doesn’t necessarily mean you should. What is the current state of these decorators and native class syntax? Are they production ready, or bleeding edge? What is the recommended path?

There are two separate considerations here - the stability of the language features themeselves, and the performance and readibility of the polyfills for the features.

Syntax Stability

If you are concerned with stability, class fields and decorators still haven’t made it all the way through the TC39 process (Fields are currently stage 3, decorators are stage 2), which means they still could change significantly. There was even a recent counter-proposal which was put forward that completely removed the concept of class fields, so even stage 3 proposals can be questioned and potentially changed significantly.

Assuming that there aren’t massive changes to the fields/decorators syntax, however, the ember-decorators project is dedicated to providing a stable API as the spec evolves. We haven’t seen too much churn between versions, and are expecting to be able to update from the Stage 1 decorators transform to the Stage 2 transform as soon as Babel releases it, without any breaking changes.

Performance and Debugability

If you’re okay with using cutting edge technology, the other major concerns are performance and debug-ability.

On the performance side of things, the current Stage 1 Babel transforms for decorators are not ideal because the code to decorate classes gets included in each individual Javascript module which uses decorators. This is not that much code by itself, but in large apps with many hundreds or thousands of collective modules it could add up quickly. With that in mind, apps and addons that are that are size-sensitive should avoid using decorators for the time being.

On the debug-ability side, class fields and decorators produce some gnarly code - if you’re familiar with the output of the async/await transform, it’s probably on the same level of complexity. It is definitely possible to debug and find your way around the output once you get a feel for it, but like all transpiled code it can be confusing, especially for someone who hasn’t seen it before.

TL;DR

Class fields and decorators are still experimental technologies in Javascript, but the ember-decorators project is dedicated to providing as much stability as possible as they change through the TC39 process. You can use the current transforms and decorators as far back as Ember 1, but if you are concerned with stability or performance you should probably wait until they move forward.


#2

Thank you for this great write-up @pzuraq!


#3

As an aside, this issue affects roughly any code that Babel currently uses “helpers” for (which is quite a lot of things). I think we should do some work in the build pipeline to reduce this issue…

Are the sourcemaps usable?


#4

As an aside, this issue affects roughly any code that Babel currently uses “helpers” for (which is quite a lot of things). I think we should do some work in the build pipeline to reduce this issue…

That would be awesome, and I think would remove that barrier almost entirely!

Are the sourcemaps usable?

I haven’t been able to get sourcemaps to map directly to pre-transpiled code, but I also haven’t been able to do it for async/await code in tests. Definitely would help if we could get them working!


#5

Thank you! How much would the extra babel-transpilation cost in kilobytes?


#6

You can see the transpiled output for a class here (targeting latest browsers, if you target older browsers it’ll also include some code to transpile class itself). Running the decorator code through gzip and uglification results in a final file size of 293 bytes, so about 0.25 kbs per file that includes decorators.


#7

Just wanted to chime in and express how much I’m enjoying the Reader’s Questions series. I share them with my team. They’re insightful and read very well.


#8

I’ve been exploring some enhancements Dependency Injection-y APIs in some of my router work lately, and one thing my API requires is the ability to lookup a class via the container/owner (via factoryFor) and determine ALL of the injections that it’s asking for. Now, if I had an API like Ember’s inject.service() API, which are essentially computed properties installed on the prototype, it would require my library to inspect the class’s .proto() prototype and loop through all the fields and collect information on all of the properties that appear to be injections (I think this is how Ember Data’s DS.attr() API works?)

Now, pretty much everyone on or near the core team tells me "please god don’t make another API that requires looking at .proto()" and I just have to take their word that it’s either non-performant, or archaic, or just Feels Wrong™ because I’m using the prototype for class level concerns when properties on the prototype are ideally only meant for consumption by instances of the class (and I need to know the dependencies a class specifies before I’ve created an instance). And as an alternative, someone usually suggests a decorators-centric API, e.g. something like:

import RouteableComponent from 'hype-hype-hype';

export default class extends RouteableComponent {
  @inject adminUser; // some data loaded from a route
}

Without getting distracted about why I’m exploring this particular API, is a decorator-centric API a good choice for me? If my API grabs the RouteableComponent subclass via factoryFor, how will the decorator-centric API make it easier to query the injections a class has specified? Does the decorator API give me access to the class? Will that access allow me to store some meta information on a (polyfill-able) WeakMap? Will all of this still be possible when subclasses are involved (i.e. both subclass and parent class specific injections via decorators)?


#9

It sounds like decorators could be a good fit for what you’re trying to do. Putting aside the Ember object-model, there isn’t really any other way to add pure metadata to a class’s prototype or constructor - fields get assigned to instances, so methods and getters are the only way to add something to either. I suppose you could use static fields on the constructor:

export default class extends RouteableComponent {
  static routeParams = ['adminUser'];
}

This mirrors the positionalParams syntax for components today, but feels a touch less declarative that a decorator + class fields would.

I do think using getters like inject.service does would probably be the best pattern if you could set some sort of general DI object on the class, like the owner/container. If not though, there are a number of ways decorators could be used to add this metadata. I think your best bet would be to use a WeakMap:

let injectionMap = new WeakMap();

function injectionsFor(klass) {
  if (!injectionMap.has(klass)) {
    injectionMap.set(klass, []);
  }

  return injectionMap.get(klass);
}

function inject(target, key, desc) {
  injectionsFor(target).push(key);

  return desc;
}

This would likely have to be done through finishers in stage 2 since decorators no longer have access to the prototype directly, but should still be possible.