The Guide to Promises in Computed Properties

“How should I deal with async one-to-one relationships in computed properties?”

“Are templates promise-aware?”

“Promises in computed properties cause me a lot of trouble”

Async computed properties can make you want to tear your hair out.

Luckily, there is a way to keep sane. Let’s start!

Async data loading common patterns

Data fetching should happen at the route level. Every URL segment has an associated route, and each route is offered various hooks that will wait for asynchronous operations to resolve. These are model(), beforeModel() and afterModel().

This is extremely useful as all data is resolved by the time the template is rendered. The template now has access to fresh, available in-store data.

However, how about async calls that are initiated by an Ember Data model?

<p>
  Is main ingredient available?
  {{ model.ingredients.main.isAvailable }}
</p>

Or data-loading components, that independently trigger async data loading?

And data paths that don’t match a route path? For example, sifting through comments in a post route?

You might be tempted to think that the comments relationship should be loaded in an RSVP.hash at the model() hook. I don’t think that is a good solution. For further thoughts on this topic see How to Load Multiple Models in a Single Route.

Yes, all of these are valid cases of loading data outside of the route.

Next, we will see how to load async data in our templates/controllers and components.

Filter based on asynchronous hasMany relationships

Promises are a fantastic abstraction for dealing with async. While elegant, they can feel contrived when we need them as a result of a computed property.

To explore different scenarios of promise resolving in templates, we are going to introduce our sample Recipes app (available on Github). It consists of Recipes that have many Ingredients. And Ingredients have many Tags. All relationships are async.

We will explore ways of filtering data through async relationships, starting by a classic example: filter vegetarian ingredients of a recipe.

Approach #1: Setting a property

We observe a property and set another one:

_filter1: observer('ingredients.@each.vegetarian', function() {
  this.get('ingredients').then(ingredients => {
    this.set('filter1', ingredients.filterBy('vegetarian'));
  });
})

I discourage you from using observers, though.

A specialized component may want to leverage its lifecycle hooks and trigger loading for instance on didReceiveAttrs:

// app/components/ingredient-display.js

export default Ember.Component.extend({

  didReceiveAttrs() {
    this._super(...arguments);
    this.get('ingredients').then(ingredients => {
      this.set('filter1', ingredients.filterBy('vegetarian'));
    });
  }

});

Another variant is using a throw-away computed property.

_filter1: computed('ingredients.@each.vegetarian', function() {
  this.get('ingredients').then(ingredients => {
    this.set('filter1', ingredients.filterBy('vegetarian'));
  });
})

The upside: we benefit from computed property caching. The downside: we need to “tickle” _filter1 to wake up and do its job of setting filter1.

{{ _filter1 }}
<ul>
  {{#each filter1 as |elem|}}
    <li>{{elem.name}}</li>
  {{/each}}
</ul>

Not very appealing so far.

Approach #2: Using plain ol’ promises

Hey, just return a promise!

filter2: computed('ingredients.@each.vegetarian', function() {
  return this.get('ingredients').then(ingredients => {
    return ingredients.filterBy('vegetarian');
  })
})

This really doesn’t work well. There are no properties to check the promise state and no return value that the computed property can use.

An each loop with this property won’t yield anything:

<ul>
  {{#each filter2 as |elem|}}
    <li>{{elem.name}}</li>
  {{/each}}
</ul>

If filter2 were an Ember Data async relationship, it would indeed give out the correct result and render the <li>s.

What the …?

Are templates “promise-aware” or “Ember-Data-promise-aware” or what?

The guides mention nothing about this because they are not promise-aware in any respect. Templates know nothing about promises. Some helpers might, but each definitely doesn’t.

Ember best practices delivered straight to your inbox? Tell me where:

(One e-mail every month. No BS. Unsubscribe anytime!)

Approach #3: Special promise-backed objects

For its async relationships, Ember Data uses a kind of promise called DS.PromiseArray which really is a promise but behaves like an array!

In other words, a proxy:

As the array-like interface is preserved, data bindings in the template are simply updated when the promise resolves! This is exactly why the each loop in approach #2 works well with Ember Data.

Before: recipe.ingredients behaves like an array of empty models (the length is known beforehand because of the amount of ingredient ids that came with the relationship in Recipe)
After: When Mirage responds, the promise is resolved and we now have the array filled up

Back to our recipes example, the filter now looks like:

filter3: computed('ingredients.@each.vegetarian', function() {
  return DS.PromiseArray.create({
    promise: this.get('ingredients').then(ingredients => {
      return ingredients.filterBy('vegetarian');
    })
  });
})

Works great!

For objects, Ember Data uses DS.PromiseObject.

Both Ember Data proxies are based on their corresponding Ember counterparts Ember.ObjectProxy and Ember.ArrayProxy.

Remember we said our plain old promise had “no properties to check the state”? These proxies conveniently mix in Ember.PromiseProxyMixin which adds promise states: isFulfilled, isRejected, etc.

But what if we can’t (or don’t want to) convert our plain promises into proxies? Is there perhaps a helper solution?

Approach #4: Friendly helpers

That’s right, whether we have PromiseProxyMixin-backed promises or just simple promises, there’s an excellent set of helpers appropriately named ember-promise-helpers.

“Ember Promise Helpers allow you to work with Promises easily in your Ember templates, without wrapping your objects with something like Ember.PromiseProxyMixin”

Let’s apply these helpers to the rogue promise in approach #2:

<ul>
  {{#each (await filter2) as |elem|}}
    <li>{{elem.name}}</li>
  {{/each}}
</ul>

It now renders correctly!

What happens if the promise is rejected?

This is a considerable drawback. We are not operating at the right level to deal with the error: e.g. perform a transition, display an error message, etc.

I proposed a solution to this problem in a pull request: Catch action for the await helper.

The await helper would include a catch action through which it yields control back to the component, controller or route:

<ul>
  {{#each (await filter2 catch=(action (mut filter2Error))) as |elem|}}
    <li>{{elem.name}}</li>
  {{else}}
    {{#if filter2Error}}
      {{filter2Error.message}}
    {{/if}}
  {{/each}}
</ul>

Approach #5: Ember Concurrency

A safer alternative to approach #1 (waiting for promises and this.setting – on a potentially destroyed component) is the extraordinary Ember Concurrency library.

Using generator functions we can rewrite our logic using tasks:

filterTask: task(function*() {
  const ingredients = yield this.get('ingredients');
  return ingredients.filterBy('vegetarian');
})

Clean syntax! How is that “task” invoked, though?

Typically, we’d use the provided perform helper. For example:

<button onclick={{perform filterTask}}>Perform task</button>

In this example, however, we need the task to be invoked as we render the template, displaying its output. As a matter of fact, we need to display its output anytime any vegetarian property changes.

Thus we will perform the task right from a computed property:

filter4: computed('ingredients.@each.vegetarian', function() {
  return this.get('filterTask').perform();
}),

filterTask: task(function*() {
  const ingredients = yield this.get('ingredients');
  return ingredients.filterBy('vegetarian');
})

Tasks return a promise-like object which can easily be handled with the await helper!

<ul>
  {{#each (await filter4) as |elem|}}
    <li>{{elem.name}}</li>
  {{/each}}
</ul>

Try all these out in the sample Recipes app:

A no less important use-case is filtering properties on promise-objects…

Filtering with an async belongsTo

We start from a list of ingredients and want to find all ingredients that belong to a stew recipe.

First attempt:

filter1: computed('model', function() {
  return this.get('model').filter(ingredient => {
    return ingredient.get('recipe').then(recipe => {
      return /stew/i.test(recipe.get('name'));
    });
  });
})

Result:

Nah. It didn’t filter anything! What is the problem?

Returning a promise in a non-promise-aware filter function takes each promise as a “truthy” object and therefore does not filter anything out.

This one is trickier. We need to wait for each element (promise) in the array – that’s why we use RSVP.filter.

Following what we learned…

filter2: computed('model', function() {
  const promise = RSVP.filter(this.get('model').toArray(), ingredient => {
    return ingredient.get('recipe').then((recipe) => {
      return /stew/i.test(recipe.get('name'));
    });
  });

  return DS.PromiseArray.create({
    promise: promise
  });
})

Bonus!

Let’s try this now with Ember Concurrency:

filter3: computed('model', function() {
  return this.get('filterTask').perform();
}),

filterTask: task(function*() {
  const ingredients = this.get('model');
  yield all(ingredients.mapBy('recipe'));
  return ingredients.filter(ingredient => {
    return /stew/i.test(ingredient.get('recipe.name'));
  })
})

Neat!

Back to the sample app…

Preloaded data Q&A

  1. What is returned when async recipe.ingredients, once loaded, is called a subsequent time?

    // (console where $E is `Recipe` with id=1)
    
    > $E.get('ingredients.isFulfilled')
    > true
    

    Answer: It returns the resolved DS.PromiseArray promise, whose content property is an Ember Data mutable array (DS.ManyArray)

  2. How about calling recipe.ingredients when the route has previously side-loaded ingredients via { include: 'ingredients' }?

    // app/routes/recipes.js
    
    export default Ember.Route.extend({
      model(params) {
        return this.store.findRecord('recipes', params.id, { include: 'ingredients' });
      }
    });
    

    Answer: It still returns the resolved DS.PromiseArray promise, whose content property is a DS.ManyArray

  3. What does store.peekAll return?

    // (console)
    
    > $E.store.peekAll('ingredient').get('isFulfilled')
    > undefined
    

    Answer: It returns a DS.ManyArray

  4. What does recipe.ingredients return with async: false?

    // app/models/recipe.js
    
    export default Model.extend({
      name: attr(),
      ingredients: hasMany({ async: false })
    });
    

    Answer: It returns a DS.ManyArray

    If you make this change in the Recipes sample app and keep treating ingredients as async things will blow up: TypeError: this.get(...).then is not a function

Recommendations

As discussed in “Should Components Load Data?” main models backing a template (those driven by the URL) should always be loaded in routes. 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 the techniques discussed in this article.

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

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)