Avoid an insidious Ember.js gotcha by always properly listing all the dependencies of a computed property
November 19, 2016Ember.js computed properties are a powerful feature of the Ember.js framework that is essentially an abstraction around a property getter (with built-in caching). The body of the computed property is evaluated whenever the value is requested; the returned value is cached until one of the other properties that is depended on changes. For instance consider the following simple Ember object:
let Person = Ember.Object.extend({
firstName: 'Betty',
lastName: 'Jones',
fullName: Ember.computed('firstName', 'lastName', function() {
return `${this.get('firstName')} ${this.get('lastName')}`;
})
});
Person.get('fullName')
evaluates to 'Betty Jones'
. The value is cached until firstName
or lastName
is changed. But for this to work, we need to manually list out the properties that fullName
depended on. Listing out all of the properties a computed depends on like this can be a fairly tedious task. It can be tempting to skip a few, espeically if you can say to yourself that certain properties would always change together. For instance, say we want to add another computed property:
Person.reopen({
json: Ember.computed('fullName', function() {
return JSON.stringify({
firstName: this.get('firstName'),
lastName: this.get('lastName')
})
})
});
Whenever firstName
or lastName
changes, fullName
is assured to change. It seems like we have just saved ourselves some typing, right? Though the output would be correct for now, this post is about why this is a bad idea, since this will not always be the case. There is a big gotcha that can happen if you do this, which manifests itself as computed properties not getting updated.
To illustrate an instance of this gotcha in action, let's suppose we have a cut-down shopping cart with a subtotal and taxes. We also make another computed property that is just the subtotal with a dollar sign prepended to it. Taxes only change when the subtotal changes, so we're going to make a formatted subtotal depend on only the taxes:
Note that is this the same concept we used for the Person example above. Clicking "Add 100" increases the subtotal by 100 (try it out). In this example, we get away with not having the proper dependencies on displayedSubtotal
, and everything seems to work as it should. But let's see what happens when we make a small change and remove the display of taxes:
displayedSubtotal
doesn't update anymore! To understand why, we must realize that computed properties are not evaluated unless something explicitly requests the property, e.g. a call to Ember.get
or a reference to the property in a template. Since nothing in the second example calls the getter on taxes
, Ember doesn't maintain a cached value for it. Thus the cached value displayedSubtotal
is never invalidated because it only happens when the cached value taxes
gets invalidated, and there is never a cached value to invalidate. If the dependent properties of displayedSubtotal
are listed correctly as 'subTotal'
, this bug is prevented.
Though this example seems a bit contrived, it's actually fairly easy to get encounter due to the sheer number of computed properties a large application can have, and because Ember doesn't emit any warnings about computed properties not having their dependencies listed properly. Additionally, this bug may stay under the radar until a seemingly unrelated change is made (e.g. a modification to a template, an observer, or anything else that calls a getter on our faulty property's dependencies). If you ever encounter a change which cases a computed property to stop updating, be sure to comb through your computed properties for incorrectly listed dependencies, even if the property is seemingly unrelated to the change.
This gotcha can manifest in other subtle ways, such as in the following example:
Here, we have the faulty displayedSubtotal
property as before, but taxes
is being evaluated and displayed. Clicking 'Add 200' will add 100 to the subtotal twice, but displayedSubtotal
seems to lag behind by 100 — very strange behaviour indeed. Here, though taxes
is being evaluated, it is only done during the rendering of the template, after both add operations have taken place.
The call to this.get('displayedSubtotal');
in the middle of the two operations refreshes the cached value of displayedSubtotal
. At this time, taxes
has an unknown value &mbash; the cache has expired and nothing calls its getter to refresh it. When the second add operation takes place, the cached value taxes
would usually be once again invalidated, and displayedSubtotal
, being a dependent property, would get recomputed. However, Ember does not invalidate caches if a dependent property goes from one unknown value to another, as taxes
does here. Thus, displayedSubtotal
doesn't get recomputed a second time and remains stuck one value behind.
Again, this example may seem fairly contrieved. However, in larger Ember applications it is easy to make these simple oversights in dependent properties, and the bug may not manifest itself until a seemingly unreleated change is made. If we get rid of the call to this.get('displayedSubtotal');
, everything seems to work fine. However, adding any such call between the two this.set('subTotal', this.get('subTotal') + 100);
would cause this bug to reappear. Imagine if you added some logging that makes the get call 'subTotal'
changed. This would cause the bug to suddenly appear again, and it's a very strange occurence to have logging create a bug in the main application logic. It could also be very unintuitive to look for computed property declaration oversights when this happens.
Because of the potential unintuitive buggy behaviour, it is Ember.js best practise to ensure all computed properties have their dependencies properly declared, even if it can sometimes seem like missing one could not make a difference.