Actions best practices, should we even use the actions hash at all?


#1

My team does a great job at testing, we have acceptance, integration, and unit tests for our Ember App. Sometimes we have found it necessary to modify our coding practices to make the code more easily testable, which I think is a good thing. It’s hard to test actions properly, so we have settled on this paradigm for using actions in a way that is more testable (we are using Babel to make use of the spread operator):

// awesome-component.hbs template
<a {{action "doSomething"}}>Click me!</a>

// awesome-component.js code
  actions: {
    doSomething() {
      this.doSomething(...arguments);
    },
  },

  doSomething() {
    // code goes here
  },

Now the action is just passing the arguments to a property on the component. This gives us much easier access in testing, and we can more easily stub the method in the tests. It also allows us to reuse the same component more often without dealing with lots of edge cases. If someplace in our app needs the same component, but wants doSomething to do something different, we can overrided doSomething by passing a method with the same name into the component as a property when it is called in the template.

{{awesome-component
  doSomething=(action doSomethingElse)
}}

If I add the quotes to the name of the method in the action, it looks for the method in the actions hash. If I don’t, it uses whatever the property is that has that name. If I don’t use quotes, and access the method directly as a property of the component, I need to use the (action) helper to bind the context of the method.

Ok, so that is the situation. My question is, why should we ever use the actions hash? Why not just use the component properties directly? For example:

// awesome-component.hbs template
<a {{action (action doSomething)}}>Click me!</a>

// awesome-component.js code
  doSomething() {
    // code goes here
  },

There are so many different ways to call and use and pass actions, I think it would be better if we just always did it this way and never used the actions hash. Am I missing something, or should we consider deprecating the actions hash?


#2

This is beginning to become a contentious issue on our team, and I was wondering if a core team member could weigh in on this. Can you envision the actions hash being deprecated one day? Why is the actions property treated specially?

Ping @rwjblue , I already owe you a few beers already, maybe I can move into keg territory.


#3

Hey @RustyToms,

I’ll take a stab at this issue, but I’d like to first specifically address the desire to “future proof” your code (“should we deprecate the actions hash”, “can you envision the actions hash being deprecated”).

Future proofing

As written down in our FAQ about future proofing your application, that is why we have the release process we have, from the six week cycle, to the deprecation cadence and Guides, and LTS releases, our hope is that you can smoothly upgrade your application by addressing deprecations as they come.

So in that sense, I would advise to keep doing business as usual. The Ember.js Guides are also maintained by the learning team, which tries to make sure it reflects the currently advised practices.

Why the actions hash

The tl;dr is: namespacing.

For what now are mostly historial reasons, the API surface of Ember.Component is quite wide. What this means in practice is that when people were creating a destroy() action to destroy some item in a list, for example, and they overrode the built-in one, leading to all sorts of zany memory leaks and things not being torn down :stuck_out_tongue:

You asked, should we ever use the actions hash? Yeah, I content that you still should always use it. Is it possible not to use the actions hash? Also yes, but you will have to provide additional education on the pitballs, like which action names you can’t use and such.

The future

I want to mention this, not as motivation to avoid the current API (which should be obvious by now ;P) but to show that we took this into consideration when designing the newer minimal API available in Glimmer components.

Despite the need for RFCs to officialise the API, as well as RFCs for their integration into Emberjs proper, you can look at Glimmerjs components to have an idea of our current proposal.

Notice that they are a new Component implementation, with several important differences in semantics, syntax (ES6 classes, decorators, and such), and API, providing what we think is useful and flexible API that doesn’t get in your way.

When/If these components land in Emberjs after the RFC process, it won’t mean the deprecation of current components. And if/when current Ember components get deprecated, you will get plenty of notice, and a detailed migration path.

Too many words, give me the juice

You should continue using the actions hash, as that API avoids overriding important methods in the Ember.Component base class.


#4

If having to type out the action/method names is becoming a burden, you could look at using a hidden feature of the action system to proxy the action to a method. ActionHandler is responsible to looking up and calling functions on the action hash. If it can’t find the action it checks for a target property to delegate action calling to

Be aware that this doesn’t work when making an action closure with a string argument (on-toggle=(action "toggleAsMethod") because ember-glimmer.helpers.action wants to resolve it to a function by looking at the actions hash, which doesn’t have it. An action closure using the method directly is fine (on-toggle=(action toggleAsMethod)) as it’s looking at the component directly for the function

An alternative is to use metaprogramming (mutate the object before calling Component.extend, decorators) to add functions to the action hash as if you had done it by hand


#5

Hey thanks so much for the very detailed reply, sorry it took so long for me to respond. It’s not the answer I wanted to hear :smiley:

But very neat to look at the glimmer component proposal. That will definitely require a lot of refactoring for us, as we sometimes depend on optional arguments overwriting properties, that basically serve to provide a default value. And we overwrite properties that we want to stub in tests as well, by passing the stubs in as arguments when we render components in integration tests