Karol Galanciak - Ruby on Rails and Ember.js consultant

Ember Tips: Computed Properties and Arrow Functions? Not a Good Idea

Arrow function expressions were definitely a great addition in ES6 and thanks to tools like babel the new syntax has been quite widely adopted. Besides more concise syntax, an interesting thing about arrow function expressions is that they preserve the context, i.e. they don’t define their own this, which was sometimes annoying and resulted in assigning that or self variables to keep the outer context that could be referred inside functions. As great as it sounds, arrow function expressions cannot be used in all cases. One example would be Ember computed properties.

Arrow Function Expressions - A Quick Introduction

Let’s start with a quick introduction to arrow functions. Before ES6, anytime we were using function expressions and wanted to refer this from outer context, we had to do some workarounds which are (arguably) a bit unnatural, especially comparing to other major programming languages.

Let’s do some pseudo-object-oriented programming with JavaScript (ES5) to illustrate a possible issue with function expressions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Order() {
  this.id = Math.floor((Math.random() * 10000000) + 1); // don't do it in a production code ;)
  this.items = [];
}

Order.prototype.addItem = function(item) {
  this.items.push(item);
}

Order.prototype.logItems = function() {
  this.items.forEach(function(item) {
    console.log("item description: " + item.description + " for order with id: " + this.id);
  });
}

var order = new Order();
order.addItem({ description: 'Glimmer 2 rockzzz' });
order.logItems();  // whooops

We have a simple class-like functionality using constructor function and prototype to implement Order with some questionable ( ;) ) way of assigning id and some items. We can add more items with Order.prototype.addItem function and we can log them with Order.prototype.logItems function.

But there’s a problem: logItems function doesn’t log id, but logs undefined instead. Why is that?

Function expressions create their own context and define own this, so it no longer refers to the outer context, which is the order instance. There are several ways to solve this problem.

The most obvious is to assign outer this to some other variable, like that or self:

1
2
3
4
5
6
Order.prototype.logItems = function() {
  var self = this;
  this.items.forEach(function(item) {
    console.log("item description: " + item.description + " for order with id: " + self.id);
  });
}

You can also pass outer this as a second argument to forEach function:

1
2
3
4
5
Order.prototype.logItems = function() {
  this.items.forEach(function(item) {
    console.log("item description: " + item.description + " for order with id: " + this.id);
  }, this);
}

You can even explicitly bind outer this to callback argument inside forEach function:

1
2
3
4
5
Order.prototype.logItems = function() {
  this.items.forEach(function(item) {
    console.log("item description: " + item.description + " for order with id: " + this.id);
  }.bind(this));
}

All these solutions work, but aren’t really that clean. Fortunately, since ES6, we can use arrow function expressions which preserve outer context and don’t define own this. After little refactoring Order.prototype.logItems could look like this:

1
2
3
4
5
Order.prototype.logItems = function() {
  this.items.forEach((item) => {
    console.log("item description: " + item.description + " for order with id: " + this.id);
  });
}

Much Better!

As great as it looks like, it may not be a good idea to apply arrow function expressions everywhere, especially for Ember computed properties.

Ember Computed Properties And Arrow Functions? - Not A Good Idea

Recently I was doing some refactoring in one Ember app. The syntax in one of the models was a bit mixed and there were some function expressions and arrow function expressions which looked a bit like this:

app/models/user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Ember from "ember";
import Model from 'ember-data/model';

export default Model.extend({
  fullname: Ember.computed('firstname', 'lastname', function() {
    return `${this.get('firstName')} ${this.get('lastName')}`;
  }),

  doThis: function() {
    // some logic goes here
  },

  doThat: function() {
    // even more logic
  },

  doYetAnotherThing(args) {
    // more logic
  }
});

So I decided ES6-ify entire syntax here and ended up with the following code:

app/models/user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Ember from "ember";
import Model from 'ember-data/model';

export default Model.extend({
  fullname: Ember.computed('firstname', 'lastname', () => {
    return `${this.get('firstName')} ${this.get('lastName')}`;
  }),

  doThis() {
    // some logic goes here
  },

  doThat() {
    // even more logic
  },

  doYetAnotherThing(args) {
    // more logic
  }
});

And how did this refactoring end up? Well, instead of a proper fullName I was getting undefined undefined! That was surprising, but then I looked at the changes and saw that I’m using arrow function expressions in computed properties and referring there to this, which won’t obviously work for the reasons mentioned before. So what are the options for computed properties?

The first one would be to simply use good ol’ function expressions:

app/models/user.js
1
2
3
4
5
6
7
8
import Ember from "ember";
import Model from 'ember-data/model';

export default Model.extend({
  fullname: Ember.computed('firstname', 'lastname', function() {
    return `${this.get('firstName')} ${this.get('lastName')}`;
  })
});

But if you don’t really like it, you may define explicit getter:

app/models/user.js
1
2
3
4
5
6
7
8
9
10
import Ember from "ember";
import Model from 'ember-data/model';

export default Model.extend({
  fullname: Ember.computed('firstname', 'lastname', {
    get() {
      return `${this.get('firstName')} ${this.get('lastName')}`;
    }
  })
});

And the last option, my preferred one: unleashing the power of ES7 decorators and using ember-computed-decorators addon. That way we could define fullName computed property in the following way:

app/models/user.js
1
2
3
4
5
6
7
8
9
10
import Ember from "ember";
import Model from 'ember-data/model';
import computed from 'ember-computed-decorators';

export default Model.extend({
  @computed('firstName', 'lastName')
  fullname(firstName, lastName) {
    return `${firstName} ${lastName}`;
  }
});

which looks just beautiful ;).

Wrapping Up

Even though arrow function expressions are very convenient to use, they can’t be used interchangeably with function expressions. Sometimes you may not want this inside a function to preserve outer context, which is exactly the case with Ember computed properties.

Comments