The Guide to Promises in Computed Properties

Last reviewed on July 26, 2019 with Ember Octane

This is a full rewrite of the article reflecting my and the community’s experiences using computed properties with promises.

Should we use promises in computed properties?

In general, the answer is no.

The need to return a promise in a computed property very likely originates in Ember Data, where relationships are async by default and encourage this pattern. Async means that the relationship will always be accessed asynchronously (through a promise).

export default class RecipeModel extends Model {
  @hasMany() ingredients;
  // ^ equivalent to @hasMany({ async: true }) ingredients;
}

The relationship ingredients will be accessed via recipe.ingredients.then(i => ...).

Trying to derive a value from ingredients, such as vegetarian ingredients, could look something like this:

vegetarianIngredients = computed('ingredients.@each.vegetarian', function() {
  // this returns a promise
  return this.get('ingredients').then(ingredients => {
    return ingredients.filterBy('vegetarian');
  });
})

Computed properties are essentially accessors, which serve as an implementation of the Uniform Access Principle.

All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.

Clients of the Recipe model will call vegetarianIngredients regardless of whether it’s a real property (storage) or a computed property (computation). The key point is that both are expected to return the actual value, i.e. an array of vegetarian ingredients.

First issue

But the vegetarianIngredients property above is not returning an array of vegetarian ingredients – it’s returning a Promise as the value.

Aside from introducing a new naming convention there is no way to distinguish a standard computed property vs a “promise” computed property. In addition, promises don’t work in templates. At least not without a specific helper like async-await.

Second issue

If that is the case, why does the recipe.ingredients async relationship work perfectly well in the following template?!

{{#each recipe.ingredients as |ingredient|}}
  {{ ingredient.name }}
{{/each}}

How is this template able to loop through a promise? Something specific to the each helper?

No. Ember Data relationships do not return promises! It uses a promise proxy object (mixes in PromiseProxyMixin) which behaves like an array but kicks off a network request and populates this array-type object once that promise is fulfilled.

The issue here is a property triggering a side-effect. By their nature, computed properties are pure functions and should behave as such: idempotent and side-effect free.

Here is an example of another framework that doesn’t encourage working with async in an accessor:

[…] a getter that returns a Future should not kick-off the work represented by the future, since getters appear idempotent and side-effect free

Computed properties should be simple and predictable. Calling recipe.get('ingredients') or recipe.ingredients should return that value and only that value. After years of working with Ember Data in different scenarios, I have come to believe that “async by default” in Ember Data is unhealthy.

The fine folks at EmberMap have made a great case: The case against async relationships in Ember Data.

Our main gripe with this API is that it combines the concerns of local data access with remote data fetching into a single method call. This makes it harder for developers to write expressive, intention-revealing code, especially because data loading is such a non-trivial part of developing Ember applications.

[…]

With async relationships, calls to .get can now introduce asynchrony into our applications in subtle ways. Asynchronous code is one of the things that makes JavaScript UI development so challenging, and async relationships will inevitably lead to surprises. Asynchrony should be dealt with explicitly and deliberately in our applications.

They explain how their Ember Data Storefront add-on helps alleviate this issue. Among many goodies, it introduces a specific API called load to fetch async resources, such that get always returns a local value (“Different APIs for data access and data loading.”)

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

The solution

Are you wanting to derive from an Ember Data async property? If so, my first recommendation is to use Storefront, specifically the Loadable mixin.

Once relationships are made sync and loaded, standard computed properties can be created as usual!

vegetarianIngredients = computed('ingredients.@each.vegetarian', function() {
  return this.get('ingredients').filterBy('vegetarian');
})

These properties can be used in templates without any underlying magic:

{{#each vegetarianIngredients as |ingredient|}}
  {{ ingredient.name }}
{{/each}}

If we want to render a newly-fetched relationship (or any other promise for that matter) we can use Ember Concurrency:

loadIngredients = task(function*() {
  yield this.recipe.load('ingredients');
});

(Notice Storefront’s load API.)

Now our template can look like this:

{{#if loadIngredients.lastSuccessful}}
  {{#each vegetarianIngredients as |ingredient|}}
    {{ ingredient.name }}
  {{/each}}
{{/if}}

Where the loading can be triggered manually with a button in the UI

<button {{on 'click' (perform this.loadIngredients)}}>
  Load ingredients
</button>

or via a component lifecycle hook:

import Component from '@glimmer/component';
import { task } from 'ember-concurrency';

export default class RecipeComponent extends Component {

  constructor() {
    super(...arguments);
    this.loadIngredients.perform();
  }

  loadIngredients = task(function*() {
    yield this.recipe.load('ingredients');
  })

  get vegetarianIngredients() {
    return this.recipe.ingredients.filterBy('vegetarian');
  }

}

Recommendations

As discussed in Should Components Load Data? the main models backing a template (those “driven by the URL”) should always be loaded in routes, potentially sideloading relationships. Ember provides a remarkable routing system, take advantage of it and don’t reinvent the wheel!

For cases in which promise resolution in the route is unnatural (ancillary async data, contrived RSVP.hash structures) I recommend using Ember Concurrency as shown above.

Does this clarify confusion about dealing with promises? Which approach do you take? Let me know in the comments below!

Enjoyed this article? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)