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:
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
:
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:
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:
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:
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
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
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
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
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
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.