I remember when I was first getting started using Ember.js at work a few years ago there was a bunch of talk about closure actions. At the time, I understood how to use closure actions, but I never really looked into how they work. I understood that you could pass an action down the component tree and trigger it on the original component, but I had no solid understanding on how this happened. Recently, a friend of mine shared an article with me about stepping through how React handles state changes. I found looking at what was actually happening really helped me understand things a lot better than just reading the documentation. I decided to try out the same sort of idea to learn about how actions work in ember. There's parts of the underlying code that I still don't 100% understand, but I still found it gave me a lot of clarity and I hope it does the same for you.

A Brief Introduction on Closures

JavaScript has lexical scoping. I was going to generate my own example, but I think it might be best to extend, or rather alter, the example from the MDN article on closures.

function greet() {
  var name = 'Mozilla'; // name is a local variable created by greet
  function sayHello() { // sayHello() is the inner function, a closure
    console.log(`Hello ${name}`); // use variable declared in the parent function    
  }
  console.log(name);
  sayHello();    
}

console.log(name);
// undefined

greet();
// "Mozilla"
// "Hello Mozilla"

The gist is that variables are scoped to their nearest outer function. Anything within that outer function can access the variable. Anything outside of the outer function, in this case the global scope, does not have access to variables declared within the function. So here greet and sayHello have access to name but the global scope does not. These rules are slightly changed with the newer let and const syntax, but the underlying principles still hold.

function greet(name) {
  function sayHello() {
    console.log(`Hello ${name}`);    
  }
  return sayHello;    
}

var myGreeting = greet("Mozilla");
myGreeting() // "Hello Mozilla"

If we change the example to return the inner function, we can call it afterwards and the context is preserved. It might seem weird that sayHello still has access to the variables within the scope of the greet function after greet has already finished executing, but this is how closures work. MDN does a great job summarizing what is happening here:

The reason is that functions in JavaScript form closures. A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time the closure was created.

I only took small pieces from the larger MDN article on closures and lexical scope. If you are still having a hard time understanding the concepts, I highly suggest you give the article a read. It does a great job of covering both subjects.

The Example

To demonstrate closure actions we'll use a simple parent component that displays the value of its property currentValue. The parent component has a single action that it passes down to a child component.

// parent-component.js
import Component from '@ember/component';

export default Component.extend({
  currentValue: false,

  actions: {
    toggleCurrentValue() {
      debugger;
      this.toggleProperty('currentValue');
    }
  }
});
{{!-- parent-component.hbs --}}
Current Value: {{currentValue}}<br>
{{child-component toggleAction=(action "toggleCurrentValue")}}

The child component receives an action and triggers it when the button in its template is clicked.

// child-component.js
import Component from '@ember/component';

export default Component.extend({
  didReceiveAttrs() {
    debugger;
    this._super(...arguments);
  }
});
{{!-- child-component.hbs --}}
<button {{action toggleAction}}>Toggle</button>

Here's how it looks rendered on a web page:

What's passed in?

The first question I had about this was, "What's passed in to the component when we give it a closure action?". Ember components, like most component libraries, have a collection of component lifecycle hooks you can override. The one I am interested in is didReceiveAttrs.

Since the didReceiveAttrs hook is called every time a component's attributes are updated whether on render or re-render, you can use the hook to effectively act as an observer, ensuring code is executed every time an attribute changes.

As you can see above in the child-component.js I've added a debugger into the didReceiveAttrs hook. When I refresh the page, with the devtools open, it pauses.

toggleAction was passed into the child-component

If you recall from above, we passed in one argument to the child-component.

{{child-component toggleAction=(action "toggleCurrentValue")}}

Running this.toggleAction in the console when we are paused at the didReceiveAttrs hook returns a function called makeClosureAction. This sounds like exactly what I need to dig into to find out more about what a closure action really is. The next step is to jump to that function in the source.

It's hard to tell exactly what this code is doing, especially when everything is compiled down. You can see that makeClosureAction is a function that receives 5 arguments: context, target, action, processArgs, debugKey (we'll likely care less about the debugKey). At this point, we can make some guesses at what these arguments might be.

  • context / target might be what we are going to call the action on
  • action is likely the action we are trying to call, and it looks like it can be represented in as a few different types (string/function/etc.)
  • processArgs is likely the arguments that get passed into the action.

However, this is guesswork at this point. What I think would really help establish some context here is seeing what called this, and what was passed in. To do this, I set a debugger in the makeClosureAction function (see in the debugger gutter above) and refresh the page.

The Action Helper

The first thing you'll recognize when looking at the stack trace, after refreshing, is that the previous item in the call stack is a function called action. If we jump to the source, you can see that it's actually the ember template helper action. You might recognize this from what we passed in to the child component.

{{!-- we're calling the "action" helper with the argument "toggleCurrentValue" --}}
{{child-component toggleAction=(action "toggleCurrentValue")}}

After jumping to that item in the trace, one of the best things to do here might be to check what is in the current scope when the action function is called. I'm not going to dive into that, because there is quite a bit in there, but feel free to browse it yourself. I'm going to step through the function almost line by line, I might skip a few lines that are less relevant to the understanding of what action template helper is doing for us.

let { named, positional } = args;
let capturedArgs = positional.capture();
// The first two argument slots are reserved.
// pos[0] is the context (or `this`)
// pos[1] is the action name or function
// Anything else is an action argument.
let [context, action, ...restArgs] = capturedArgs.references;

The first few lines of the action function are just splitting up the arguments. Similar to components, you can pass both named and positional arguments to helper functions. The action template helper accepts two optional named arguments.

target=someProperty will look to someProperty instead of the current context for the actions hash. This can be useful when targeting a service for actions.

value="target.value" will read the path target.value off the first argument to the action when it is called and rewrite the first argument to be that value. This is useful when attaching actions to event listeners.

As the comments specify, the first item in the positional arguments will always be the context from where you are calling it. The second positional argument is the action itself and anything that follows is arguments that we want to pass into the action.

Remember we are calling the action helper like this (action "toggleCurrentValue"). There are no named arguments, but there are the 2 guaranteed positional arguments. The first positional argument is the context from which the action helper was called, the parent component. The second is the action string we passed in. "toggleCurrentValue".

context is the parent-component where we are calling the action helper from. action is the action string.
let debugKey = action._propertyKey; // We'll skip covering the debugKey
let target = named.has('target') ? named.get('target') : context;
let processArgs = makeArgsProcessor(named.has('value') && named.get('value'), restArgs);
let fn;

The next few lines continue to set up variables we'll need in order to set up the closure action. The target gets set to the named argument target if it was passed in. Otherwise, it will get set to the context, which is the first positional argument and the context in which the action helper was called. For processArgs, it builds an argument processor by calling makeArgsProcessor with the named argument value, if it's present, and any of the remaining positions arguments (anything passed after the first two arguments context and action). I won't dive into the argument processor, but it's located in the same file so feel free to explore it on your own. Lastly, it declares the variable fn, but does not initialize it.

We can begin to make some guesses about what these are going to be used for just by looking at the names and what we are initializing them with. It seems like we will be calling a fn on a target with a set of arguments processed by processArgs. But that's just a guess... let's dig in further.

if (typeof action[INVOKE] === 'function') {
  fn = makeClosureAction(action, action, action[INVOKE], processArgs, debugKey);
} else if ((0, _reference.isConst)(target) && (0, _reference.isConst)(action)) {
  fn = makeClosureAction(context.value(), target.value(), action.value(), processArgs, debugKey);
} else {
  fn = makeDynamicClosureAction(context.value(), target, action, processArgs, debugKey);
}

This snippet of code looks a bit messy because it's compiled down. If you have a hard time reading it, you can always reference the cleaner source on GitHub. This snippet initializes the fn variable with the result of calling makeClosureAction if the action has an INVOKE property on it that is a function, or if the target and actions are consts. However, this is using isConst from the glimmer-vm, and I don't have a solid understanding of what it's actually checking. Feel free to reach out to me if you have a better understanding of this. If neither of those evaluate are true, fn gets set to the result of calling makeDynamicClosureAction.

The action we passed in was just a string "toggleCurrentValue". action[INVOKE] returns undefined, but isConst(action) is true and so is isConst(target). So fn is equal to the result of calling makeClosureAction with the context, target, and action.

fn[ACTION] = true;
return new UnboundReference(fn);

Lastly, it sets the ACTION property on fn to true and returns an [unbound reference]() to it.

Making Closure

Let's jump back into the makeClosureAction function. We have the context required to understand what the parameters are.

We will again look at this function piece by piece to get a better understanding of what is going on.

true && !(action !== undefined && action !== null) && (0, _debug.assert)(`Action passed is null or undefined in (action) from ${target}.`, action !== undefined && action !== null);

In the first few lines, ember runs an assertion that you the action passed in to this function is defined and not null.

if (typeof action[INVOKE] === 'function') {
  self = action;
  fn = action[INVOKE];
} else {
  let typeofAction = typeof action;
  if (typeofAction === 'string') {
    self = target;
    fn = target.actions && target.actions[action];
    true && !fn && (0, _debug.assert)(`An action named '${action}' was not found in ${target}`, fn);
  } else if (typeofAction === 'function') {
    self = context;
    fn = action;
  } else {
    // tslint:disable-next-line:max-line-length
    true && !false && (0, _debug.assert)(`An action could not be made for \`${debugKey || action}\` in ${target}. Please confirm that you are using either a quoted action name (i.e. \`(action '${debugKey || 'myAction'}')\`) or a function available in ${target}.`, false);
  }
}

This next part is the meat and potatoes of the makeClosureAction function. It determines what context we will use for the action and sets up the action itself. The first block in the if statement checks if the action we passed in has an INVOKE property that is a function. Our action is just a string at this time so we know it won't satisfy that condition, so we jump into the else.

We grab the type of the action we passed in. For us, it's a string, but as you can see further down, it can also expect a function. If the type is neither, an assertion is raised. Since our action is a string we proceed into this block:

if (typeofAction === 'string') {
  self = target;
  fn = target.actions && target.actions[action];
  true && !fn && (0, _debug.assert)(`An action named '${action}' was not found in ${target}`, fn);
}

This sets the self variable to be the target that was passed into the makeClosureAction function. For this case, the context is the parent-component. It then looks for the action on the parent-component. You can call .actions on any ember object that uses Ember.ActionHandler; such as routes, controllers, services and components. It uses this to get the list of actions on the parent-component, and find the reference to the action we passed in, "toggleCurrentValue". If it does not find the corresponding action on the target it will raise a familiar assertion.

return (...args) => {
  let payload = { target: self, args, label: '@glimmer/closure-action' };
  return (0, _instrumentation.flaggedInstrument)('interaction.ember-action', payload, () => {
    return (0, _runloop.join)(self, fn, ...processArgs(args));
  });
};

This brings us to the closure itself. makeClosureAction returns a function, that we ultimately end up calling when invoking an action. Remember that the action helper, in our case, returns the result of calling makeClosureAction. This means it returns a function that has access the lexical scope in which it was declared.

A closure is the combination of a function and the lexical environment within which that function was declared.

Let's take a look at the none-compiled version of above.

return (...args: any[]) => {
    let payload = { target: self, args, label: '@glimmer/closure-action' };
    return flaggedInstrument('interaction.ember-action', payload, () => {
      return join(self, fn, ...processArgs(args));
    });
};

We declared an initialized the self and fn variables in the main makeClosureAction function. They are in the lexical scope of this inner function, so we still have access to them well after the makeClosureAction function finishes its execution. This last snippet uses ember run-loops join method to call the fn action on the self target with any arguments processed by the processArgs function.

Triggering the action

I hope you now know a bit more about how closure actions are set up. The thing that really clicked for me was what the closure was really for. Now that I've seen how it works it seems so clear. This kind of knowledge makes working or debugging in ember so much easier.

Let's take a quick look at the stack trace from when we trigger the action. To see this, I've added a debugger to the action toggleCurrentValue.

// parent-component.js
actions: {
  toggleCurrentValue() {
    debugger;
    this.toggleProperty('currentValue');
  }
}

When I click the Toggle button, it triggers this breakpoint.

You might recognize a few of those function names in the stack trace from above. We'll jump to the first reference to makeClosureAction.

self, and fn are preserved from when we originally called the action helper

This looks familiar! It's the inner function from the makeClosureAction function. As you can see from the image above, self is still referencing the parent-component. The ember-id has changed, but that is only because I refreshed the page a few times while writing this, and the ids are not guaranteed to be the same across loads. fn references the function toggleCurrentValue. We did not pass in any arguments so those are empty. From here ember uses join to schedule a call to toggleCurrentValue with the parent component context.

If no run-loop is present, it creates a new one. If a run loop is present it will queue itself to run on the existing run-loops action queue.

Conclusion.. or Closure

I hope you found this to be a useful exploration of closure actions in ember. The neat thing about closure actions is that once the closure has been created you can pass it as far down the as you want to and calling it will still work, since the scope is preserved by the closure. There certainly are parts of this that I haven't fully explored, but this adventure has left me a bit smarter than before and I hope it helped you too.

If you have any suggestions on how to improve this post or any topics you'd like me to explore in the future feel free to reach out to me on twitter @tylerwen.

References