Question about the Native Decorator Support feature

I’m working on the next video in EmberMap’s free What’s New in Ember series and it’s for RFC 440 Native Decorator Support, which was a feature that landed in 3.10.

I’m trying to understand what this means for day-to-day Ember developers.

It looks like the #408 Decorator support RFC preceded this one, but I’m still not quite sure if either of these meant any recommended changes to user-facing APIs.

I believe the 3.15 guides were the first to show decorators, starting with the actions example in the quickstart. The 3.14 Quickstart still shows the actions hash as the recommended way to define actions, and the classic usage of things like computed properties.

So – if anyone could help me sort through this I’d be very grateful! The point of the series is to distill what each new Ember feature means to working Ember devs, so I want to make sure the information is accurate.

From what I can gather, these RFCs were more about setting the stage for Ember’s usage of decorators in later versions (3.15), and ironing out the commitment to supporting decorators even before they moved to stage 3. If true, that means a team who upgraded to 3.10 shouldn’t be changing how they write actions or decorators just yet. Is that correct?

2 Likes

Decorators made classes viable for development. Pre 3.10, there were caveats, and polyfills and other addons you had to use to “make it all work”.

Like, I don’t know if this was valid:

class {
  property = computed('key', function() {
    return 'foo';
  });
}

but this is def valid:

class {
  @computed('key')
  get property() {
    return 'foo';
  }
}

without decorators, you didn’t really have much you could do with classes. This is actually where the ember-decorators addon (pre version 6) stepped in for pre 3.10 decorator usage of computeds and all that.

Additionally, decorators enable the use of @tracked, which landed in 3.13.

The 3.10 release of decorators (I don’t think) was pushed in front of everyone, because the combination of classes + decorators required an intermediate phase between classic and octane paradigms… and it was maybe just easier to wait until Octane (3.15), to talk about decorators in general.

One nice thing about being able to use decorators early, is that when 3.13 came around, you could just… delete lines of code by triple clicking and hitting back space (or just dd in vim) to get rid of the computed lines. Using decorators for computed properties made moving to tracked less work.

1 Like

I would summarize the easy/happy path this way:

  • If you want to use native classes and decorators, use v3.15 onwards. The reason is that you get the native classes file generators and consistent, stable decorators (for example, decorators for Ember Data models)
  • People can write their own decorators, which many addons have done
  • Octane-y apps should use on and fn instead of {{action}}. If you use ember-template-lint with the “octane” recommended set, it lints against action

In the Guides, we started showing native classes, decorators, and on/fn at 3.15 (Octane launch) because up until that point, there were caveats (too much detail to explain to new devs) and the blueprints hadn’t been changed.

I think your interpretation of the RFCs is correct - we add support in Ember and decorators work for us in Ember regardless of whether they advance past TC39 stage 3 or not.

For teams who aren’t on 3.15 yet, I am not sure what is recommended for best dev UX, but this blog post by pzuraq may help answer some of your questions: https://blog.emberjs.com/2019/02/11/coming-soon-in-ember-octane-part-1.html

4 Likes

Very helpful, thanks Jen!

Sounds like the right way to talk about decorators to the general public is to say that the relevant RFCs were mostly about laying the groundwork for 3.15.

For more advanced users who are curious about compatibility with other libraries in the JavaScript ecosystem and who perhaps want to author their own decorators, Ember has a commitment to support stage 1 decorators as currently implemented in Babel (https://babeljs.io/docs/en/babel-plugin-proposal-decorators).

One last thing to clarify. Since native classes and decorators weren’t introduced to the guides until 3.15, and the 3.15 guides don’t document Ember Components (only Glimmer Components, unless I’m missing something), then I believe there isn’t any official documentation on using native classes and decorators with classic Ember components. Is that correct? And if so, I think that makes the story easy to tell: teams shouldn’t really attempt to upgrade Ember components to use native classes and decorators, since (as you pointed out) there’s a lot of detail to explain and caveats needed to get those working.

So, if a team is upgrading their app, they shouldn’t worry about trying to refactor Ember components to use native classes and decorators, as it would be confusing and there’s no documentation on using them together. Instead, they should wait until 3.15, and if they want to refactor a component the best path would be to change their Classic Ember components to Glimmer Components (which can only be authored using ES classes and decorators).

For other classes (like Routes), the refactor to 3.15 idioms would use the same base imports, but now they’d be able to use native classes, and use decorators for things like @actions and @computeds.

Sound about right?

1 Like

unofficial, but some information is out there (for other people who may come across this thread wanting to upgrade classic @ember/component):

Re: @ember/component:

More general, but would also cover techniques useful for updating @ember/component

1 Like

As a general summary, that’s a very good one! However, less for the sake of what you say and more for the sake of other folks who come across this in reading/searching on the internet—

It depends on the size of app and scope of work involved, I think. That’s certainly the official path, and it’s a good path—but in sufficiently large apps it may make sense to make progressive passes instead of trying to do everything at once. For example, in the app I support (which is one of the largest Ember apps out there), we’re actively adopting things as they become available as we upgrade Ember versions.

Our basic path to Octane is:

  1. Native classes throughout the app (including the removal of all mixins), using ember-decorators and ember-classic-decorator for features as required on classic components. This allows us to get much better dev experience and ergonomics very quickly. It does require us to take on that third-party dependency, but it’s well worth it for being able to write native classes everywhere (even for new Ember components) while working on everything else.

  2. Rewriting templates to use explicit this and named arguments. This is not actually required at this phase, but is convenient to do here. It can actually happen anywhere between here and step 6, however—with the qualification that template-only Glimmer components require use of named arguments, because there is no this for them to reflect on (unlike “template-only” Ember components, which still have a backing class).

  3. Rewriting templates to use angle bracket invocation. This adds named arguments on the invocation side, and enables ...attributes to work.

  4. Rewriting components to use outerHTML semantics (as Glimmer components do). This comes after (3) because angle bracket invocation interoperates cleanly with classic components’ handling for attributes like class, id, and any attributeBindings values. Switching to tagName: '' (or @tagName('') requires making sure those are all manually reflected into the template.

  5. Eliminating use of classic component hooks in favor of modifiers, i.e. didReceiveAttrs to {{did-update}} or, better, dedicated modifiers for specific functionality. (We will use did-update as a stop-gap, and as we identify common patterns, we’ll migrate to those.)

  6. Rewriting classic components into Glimmer components and adopting @tracked at the same time. (These go together well because they both require fulling loading up the data flow and design of the component into your head; you might as well do them simultaneously rather than repeating the process.)

Attempting to do all of those pieces at once would be a non-starter for us, as it would leave us in limbo between pre-Octane and Octane for a much longer time and we wouldn’t be able to start until much later. However, it’s also definitely a more complicated flow, and that’s why the official recommendation to just do all of this component-by-component is a good plan for many apps. It’s just that the official plan is not the only plan and that it may not be the right plan for some applications.

4 Likes

Interesting – thanks for taking the time to write this out. Very useful.

Question for you: are you all doing this over the entire codebase? Why not leave old Ember components as they are, and just write new code using Octane? (It is possible to use both alongside each other, right?)

The ember-decorators + ember-classic-decorator addons are a little confusing to me… any chance you could share a simple example of your Step 1 above?

In particular ember-classic-decorator says

Classic components must always be marked as classic… because their APIs are intrinsically tied to the classic class model. To remove the @classic decorator from them, you can… Convert classic components to Glimmer components

We are, and the answer is: we want to move actively to a world where everything is using the new APIs, for a number of reasons:

  • better performance, which is a really big deal at this size of application, but matters in all apps—Glimmer components are much better than Ember components for performance

  • better developer experience, including teaching, which is a big deal with the sheer number of engineers we have: once we finish this transition we won’t need to teach both worlds, and our onboarding for folks coming from other frameworks will be easier

  • in the medium-term, hopefully being ready to just drop page weight as legacy parts of the Ember programming model can be deprecated and svelte’d away

We don’t want to be in the world with mixed paradigms for any longer than we have to; one of the big advantages Ember gives us is shared conventions and norms, and having to context-switch between Ember and Glimmer components, between curly- and angle-bracket-invocation, etc. reduces that a lot. Add in the developer ergonomics and performance wins that we get today, much less in a future where we can (for example) drop the Ember Component class entirely (if it became an optional feature in the framework, for example)—it’s a bunch of wins for getting us all the way there sooner rather than later.

Sure, rewriting a component class might look like this. Before:

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

export default Component.extend({
  firstName: '',
  lastName: '',

  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  }),
  
  classNames: ['user-profile'],
  attributeBindings: ['data-test-user-profile']
  
  actions: {
    updateFirstName(newFirstName) {
      this.set('firstName', newFirstName);
    },
    
    updateLastName(newLastName) {
      this.set('lastName', newLastName);
    },
  }
})

After:

import Component from '@ember/component';
import { action, computed, set } from '@ember/object';
import { classNames, attributeBindings } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';

@classic
@classNames('user-profile')
@attributeBindings('data-test-user-profile')
export default class UserProfile extends Component {
  firstName = '';
  lastName = '';
  
  @computed('firstName', 'lastName')
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  
  @action updateFirstName(newFirstName) {
    set(this, 'firstName', newFirstName);
  }
  
  @action updateLastName(newLastName) {
    set(this, 'lastName', newLastName);
  }
}

(There are other decorators for e.g. classNameBindings and individual attribute bindings, but this gives you the basic idea.)

As its README indicates, the @classic decorator can be removed from all Ember classes except Component once you move away from using all EmberObject methods on it—i.e. no this.set(...); use set(this, ...) instead. The decorator comes with lint rules and runtime checks to make sure you’re making the migration safely.

2 Likes

There is one place where we talk about using native classes in classic Ember Components - https://guides.emberjs.com/release/upgrading/current-edition/, where we lay out two paths: run the codemod for components, or don’t. The choice depends on how the team operates, how big the app is, etc. However, we do not currently show any @ember/object examples in the rest of the Guides.

Here’s some experiential info:

At Cardstack, we turned everything straight into Glimmer components as fast as possible in our most actively developed app. It’s a small app and most components were templates anyway. At a previous employer, if I was still there I would have probably only used native (Glimmer) classes for new components for a while, then run the native classes codemod on the rest of the preexisting components. Bouncing between native classes and Ember Objects in the same day is not nice dev UX. That said, it was also a small app.

Large apps would likely do better to take an incremental approach - run the codemod on the most-used components only. Run it on the others later. Write all new components using native classes & Glimmer. What is most important is to pick a strategy and communicate it. The worst case scenario would be to have a jumble of different styles indefinitely. I haven’t worked on a large Octane app myself yet, so this is based on what others have told me and my own instincts for managing big refactors.

One neat thing about the native class codemod is that you can supposedly run it , and in many cases, change the component import path to @glimmer/component and nothing will change, except unlocking the ability to use tracked.

When I say “caveats” for native classes, I mostly refer to file generators and trying to teach multiple component styles to beginners. Each style has drawbacks/differences to explain that would overwhelm someone new, but an existing app is a different story. I am not aware of any stability issues. Native classes are supported across all LTS I believe.

Last thing, I think it’s really important to make sure that people know there are codemods that do the heavy lifting. No one should need to sit and convert stuff by hand in order to move to native classes. If you are going from native class classic to glimmer, you would need to follow the migration guides to refactor your uses of positional params, this.element, and this.elementId, I think.

3 Likes

I agree with this very strongly. I should have called it out in my comments above, but we’re doing all of this with codemods. In many cases, they can do 100% of the work; in others, it’s more of an 80/20 thing—but it’s still a big win.

1 Like

Y’all rock. Thanks for the detailed replies.

So, supposing a team was going to take a route similar to what Chris laid out above, 3.10 would be the earliest one could start converting Ember Components to use ES Classes (along with pulling in ember-decorators and ember-classic-decorator)?

You can actually start on native classes as early as 3.4 with some polyfills, but 3.8 is the earliest I’d actually recommend: there are a lot of caveats for versions earlier than 3.6, so 3.8 is a good starting point as it was an LTS. 3.8 with the ember decorators polyfill works great (that’s where the app I’m working on is at present); 3.10 with them present natively is :100:.

I took the path of converting all components (and other core things to native classes and outerHTML first). Now I’m working on converting this classic native classes to glimmer. The bet for me was that getting teammates used to native class syntax and tagless components and enforcing that all new components followed this would be the right first step. I was actively warned against this by some folks in Discord, but I still think that this may have been the better decision, EXCEPT for the huge caveat that this path is not documented very well.

The next phase will be moving to GC, and that will require moving away from classic lifecycle hooks and classic decorators like @computed. It MAY be possible to move away from lifecycle hooks before GC also because it would be a good way to introduce/teach custom modifiers, but I’m not sure yet.

I used a lot of the information in this thread to make the EmberMap video on Native Decorator support – thanks so much @jenweber and @chriskrycho for all the helpful information!

https://embermap.com/topics/what-s-new-in-ember/native-decorator-support-3-10

3 Likes

There will hopefully be some unofficial but very thorough guides on just that path Soon™, as my team plans to publish the vast majority of our guides for our own app in open source contexts (though no the official guides).

if you end up publishing some guides, which space should we keep an eye on? Linkedin engineering blog? Is that where they’d end up?

They’ll almost certainly end up in the Ember Atlas, an unofficial collection of guides we’re (including but not limited to LinkedIn folks) slowly curating! They’ll go in the Octane Upgrade Section specifically. Before I ended up working on just getting us to an Octane-ready version starting back in August (stillllll going :sweat_smile:) I got the first such bit of work done, here. Lots more to come Soon™.