Using Closure Actions in Ember.js

Actions have come a long way in Ember.

The idea of abstracting plain ol' functions into the "actions" abstraction brought along several benefits.

It allowed us to develop a hierarchical structure of functions. This hierarchy of functions allowed us to "bubble-up" execution flow, thereby letting us delegate different responsibilities of one "action" in different parts of the app.

Instead of passing along functions, passing along the names of functions allowed for writing functions in a declarative way, that could allow for composable hierarchies or default fall-backs.

One caveat of this method was the absence of return values. Once a child-level component sent an action, it had no way of knowing what happened, except for the data that may trickle down via properties that are passed to it from the parent container.

It wasn't until Ember v1.13 that Closure Actions were released, and changed some of the fundamentals of action handling.

Closure Actions

The previous way of passing down an action looked like this:

{{some-component saveUser="saveUser" user=user}}

Now, closure actions provided this syntax:

{{some-component saveUser=(action "saveUser" user) user=user}}

In the 2nd example, I am using the action helper which will return a function to the saveUser property. So effectively, I am passing down a function, instead of the function name. This allows me to treat the saveUser property on the component as a function that I can directly execute. So instead of having to call sendAction('saveUser', user) I can now do this.get('saveUser')().

The main benefit of this is that I am now able to receive return values. Calling sendAction will yield no values but this.get('saveUser')() can return a value, just like a regular function call.

Another fun perk is the ability to curry your action calls. In the 2nd example above, I am passing user as the second param to the action helper. This means that whenever I call saveUser from my component, the callee will always receive user as its first param.

This is useful for attaching data from the parent-level container onto actions sent up from child components, without having to entangle the component with getting and setting that property.

A caveat

One caveat that emerges from this pattern is a break in the bubbling pattern.

To explain, lets understand the older syntax:

{{some-component saveUser="saveUser" user=user}}

If "saveUser" was not defined in the immediate parent container, Ember would look up the hierarchical tree to find an action of the same name, automatically.

Now when we do:

{{some-component saveUser=(action "saveUser" user) user=user}}

Ember will expect "saveUser" to exist in the local context, i.e. it should be available on the immediate parent. If not, things will break. Why this is, is beyond me and beyond the scope of this article frankly. Luckily, there have been efforts to solve this problem by third parties.

The route-action helper

Using the ember-route-action-helper add-on, we can safely assume that our closure actions will bubble up accordingly as they did with the previous syntax.

{{some-component saveUser=(route-action "saveUser" user) user=user}}

In this example, not only do we pass down a closure action to our component, but we also have the benefit of traversing the hierarchy of actions.

In this case, if "saveUser" is not defined in our immediate context, the route-action helper will traverse the tree of parent routes until it finds an action with the same name. Once found, it will execute and return the value of said function.

Since it does return a value though, it does not respect the old true or false return values that indicate an action for continued bubbling. That is, this helper will bubble to the first found action, exectue and return, and bubble no more.

Opinion

For the most part, this shift in functionality, IMO, is better suited for the common use cases of actions.

Although having named actions did produce the benefit of composable hierarchies, I believe the abstraction offered too much in spite of practical use.

Using functions directly, and passing them down as such, seems to agree more with the mental models we all share as JavaScript developers.

But some features of action bubbling, such as traversing the parent routes for actions, seems to be of great use and utility. The route-action helper most certainly exists for that reason.

Having actions return values is a big plus, and in my next article, I will cover how you can return promises from them, leading to interesting ways of handling them in components.