Ember.js has a great way of managing state on objects: computed properties. A computed property is basically a cached function call.
If Ember idioms are properly followed, computed properties are pure functions. A pure function, when called with the same arguments, will yield the same result with zero side effects. It’s the fact that they are pure that makes them safe to cache. As long as the inputs do not change, the result does not need to be recomputed, and the cached value can be returned.
let Person = Ember.Object.extend({
fullName: Ember.computed('firstName', 'lastNane', function() {
let firstName = this.get('firstName'),
lastName = this.get('lastName');
return firstName + ' ' + lastName;
})
});
As you can see in this example, fullName
is pure. It relies on firstName
and lastName
. The arguments before the function are the dependencies. If any of those change, we’ll need to run our function again if fullName
is asked for. But since this function is running in the context of our Person
, everything is available. We can introduce side effects by setting properties.
What’s more likely is that the dependencies change and we don’t update both their names and the spot in the function that asks for them. (You did notice the typo in lastNane
right?) Bugs like this are incredibly hard to catch, and they can cause subtle problems when a computed property doesn’t always recompute at the right time.
Having been burned by this way too many times, Will and I finally broke down and started enforcing our property purity with pure
. pure
lets us declare our dependencies and our function to use them, but it does not give us any access to the object itself. Let’s look a quick example:
fullName: computed.pure('firstName', 'lastName', (first, last) => first + ' ' + last),
Since we no longer care about the context, we can use the nice fat-arrow syntax. We only define our dependencies once, and if we get them wrong, our implementation is borked because the desired dependencies are simply not available. We will immediately see that something is wrong, rather than having a subtle bug that will show up later. We can even use this with simple @each
dependencies:
names: computed.pure('[email protected]', names => names.join(' ')),
While pure
does not support all the Ember computed property syntax, it’s a good start. Pure computed properties lower the chance of bugs and clean up the code. I’d like to see pure
become officially adopted by Ember. Using guaranteed pure properties should be the default, and the old syntax should only be used/allowed when absolutely necessary.
Feel free to grab and use the helper below and tell us what you like/don’t like about it.
pure(...args) {
const f = args.pop();
return Ember.computed(...args, function() {
const values = args.map(arg => {
if (arg.match(/@each/)) {
const segments = arg.split('.@each.');
return this.get(segments[0]).map(v => v.get(segments[1]));
} else if (arg.match(/\[\]/)) {
const segments = arg.split('.[]');
return this.get(segments[0]);
} else {
return this.get(arg);
}
});
return f(...values);
});
}
Interesting idea. I lean toward this kind of thing lately.
Maybe you would do
const f = args.pop();
instead of:
const f = args[args.length – 1];
args.removeAt(args.length – 1);
Thanks for pointing that out Daniel. Updated.
You can get similar functionality from https://github.com/kellyselden/ember-macro-helpers#computed or https://github.com/rwjblue/ember-computed-decorators#application-usage
I’ll just leave this here:
https://github.com/kellyselden/ember-macro-helpers/blob/master/README.md#computed
Kelly & lolmaus,
ember-macro-helpers looks amazing. Some others on the ember slack team recommended it after reading my blog post as well. It definitely solves some of my concerns with Ember’s computed properties. Is there a way to ensure pureness or is `this` still available? If not, I may look into how best to add it to ember-macro-helpers. Thanks for the suggestion!
To answer my own question: just using the fat arrow syntax will prevent any accidental ‘this’ usage.
This is awesome, I added this as a util functional and its working like a charm, thanks!