Andrey Mikhaylov

Andrey Mikhaylov @lolmaus

frontend developer, EmberJS enthusiast
← To blog index

PromiseProxyMixin: pure Ember alternative to ember-concurrency

7th March 2017 (last updated at 5th January 2018)

ember-concurrency is an extremely powerful and useful addon. Yet, if your only use case is fetching or sending data, there's a lighweight alternative.

This article was originally posted on Deveo blog.

When Deveo was acquired by Perforce, Deveo blog was turned down.

ember-concurrency is an exceptionally powerful add-on with numerous use cases.

The most common use case though is simply fetching or submitting data. You may be hesitant to include ember-concurrency into your app only for this use case.

The matter is that Ember has all the necessary pieces included for implementing this kind of data fetching with simplicity and efficiency while staying true to the Ember way.

Let me demonstrate on a simple example. We are going to fetch the remaining number of available requests from GitHub API:

GET http://api.github.com/rate_limit

I've chosen this particular API endpoint because it's the only one that GitHub doesn't rate-limit. :trollface:

Let's implement a data fetching method:

import Controller from '@ember/controller'
import fetch from 'fetch'

Controller.extend({
  _fetchGitHubRate () {
    return fetch('https://api.github.com/rate_limit')
      .then(response => response.json());
  },
});

I'm using ember-fetch, but it can be anything that returns a promise, for example, the ember-ajax service.

And it can happen not only in a controller, but in any other Ember entity: component, service, model, etc.

You've probably heard an opinion that returning a promise from a computed property is a bad idea. Well, with PromiseProxyMixin that's not true.

Let's create an Ember Object enhanced with PromiseProxyMixin. You can do this on the root level of your module:

import EmberObject from '@ember/object'
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'

const PromiseObject = EmberObject.extend(PromiseProxyMixin);

Now we can wrap the promise into PromiseObject. Make sure to create two distinct computed properties (CPs):

// This CP returns a simple promise
gitHubRatePromise: computed(function () {
  return this._fetchGitHubRate();
}),

// This CP wraps the promise with with `PromiseObject` 
gitHubRateProxy: computed('gitHubRatePromise', function () {
  const promise = this.get('gitHubRatePromise');
  return promise && PromiseObject.create({promise});
}),

Note the promise && part. We don't want the promise proxy to be created when the promise does not exist because it would crash in that case.

The API endpoint we're accessing returns the data in this format (fragment shown):

{
  "resources": {
    "core": {
      "limit": 60,
      "remaining": 60,
      "reset": 1486831110
    },
}

This hash will become available in the template as gitHubRateProxy.content. You can work with this property normally, as shown below:

  gitHubRate:          reads('gitHubRateProxy.content'),
  gitHubRateRemaining: reads('gitHubRate.resources.core.remaining'),
  gitHubRateLimit:     reads('gitHubRate.resources.core.limit'),

While the promise is not resolved, those properties will be undefined. Make sure to account for that when you use them downstream:

  gitHubRatePercentage: computed('gitHubRateRemaining', 'gitHubRateLimit', function () {
    const gitHubRateRemaining = this.get('gitHubRateRemaining');
    const gitHubRateLimit     = this.get('gitHubRateLimit');

    // We don't want a `NaN`!
    if (gitHubRateRemaining == null || gitHubRateLimit == null) return;

    const percentage  = Math.round(gitHubRateRemaining / gitHubRateLimit * 100);

    return `${percentage}%`;
  }),

Now you can simply use these properties in your template!

Your GitHub rate limit: {{gitHubRateRemaining}} ({{gitHubRatePercentage}})

Initially, the gitHubRatePromise CP is not consumed, and the request isn't made.

When the template is rendered, the gitHubRateRemaining computed property is accessed. This CP depends on gitHubRateProxy. The gitHubRateProxy in turn reads gitHubRatePromise.

When the gitHubRatePromise computed property is accessed for the first time, it calls the data fetching method and returns the promise.

This promise is cached, so when it is accessed again, the computed property returns the same promise, and duplicate requests are not performed. Essentially, it implements a pattern that ember-concurrency calls drop!

The promise is wrapped into the PromiseObject available as gitHubRateProxy. When the promise resolves, its return value becomes available as gitHubRateProxy.content.

Note that this approach is declarative. I. e. you don't have to do this:

didInsertElement () {
  this._super()
  this.get('fetchGitHubRateTask').perform()
}

Before the promise is resolved, gitHubRateProxy.content will be undefined. This means that while the promise is pending, the user will see nothing. Let's fix that.

PromiseProxyMixin exposes the gitHubRateProxy.isPending property. We can read it in our template:

{{#if gitHubRateProxy.isPending}}

  Retrieving GitHub rate limit...

{{else}}

  Your GitHub rate limit: {{gitHubRateRemaining}} ({{gitHubRatePercentage}})

{{/if}}

Doing this feels quite natural. Turns out, returning promises from computed properties isn't that bad! 😉

You might have already noticed a problem in this example: if a promise is rejected (due to a network hiccup, for example), it's rejected value will be cached forever. This is where ember-concurrency shines: it lets you restart a rejected task with very little boilerplate code.

We can restart our promise with a few extra lines of code. The trick is to overwrite the gitHubRatePromise computed property with a static promise:

  actions: {
    refetchGitHubRate () {
      this.set('gitHubRatePromise', this._fetchGitHubRate());
    }
  },

Calling this action will start a new network request, put its promise into gitHubRatePromise and force all dependent computed properties to recalculate! gitHubRateProxy.isRejected will be true when the promise is rejected. gitHubRateProxy.reason will contain the rejection message. Let's do it:

{{#if gitHubRateProxy.isRejected}}

  Failed to retrieve GitHub rate limit.<br>

  Reason: {{gitHubRateProxy.reason}}<br>

  <a href {{action 'refetchGitHubRate'}}>
    Retry
  </a>

{{else if gitHubRateProxy.isPending}}

  Retrieving GitHub rate limit...

{{else}}

  Your GitHub rate limit: {{gitHubRateRemaining}} ({{gitHubRatePercentage}})

{{/if}}

See the complete code sample and try it in action on Ember Twiddle:


Here you can find the ember-concurrency variant for comparison.

If you have the described logic on a component and render the component in two distinct routes, it will redownload the data every time the user switches routes.

This is likely not desirable. Instead, you want the response to be cached globally, it should be redownloaded only when explicitly told to.

The solution to this is simple: move the logic into a service. It's very convenient to subclass ember-ajax and enhance it with custom methods and computed properties.

Note that official PromiseProxyMixin docs suggest using Ember.ObjectProxy. However, it is doing some black magic with the only purpose of which is to shorten this path:

gitHubRateProxy.content.resources.core.remaining

by removing the .content segment so that it looks like this:

gitHubRateProxy.resources.core.remaining

Naturally, this black magic doesn't work for arrays. For arrays, you have to use Ember.ArrayProxy which of course doesn't work with objects. And if your promise returns a class instance rather than a hash (POJO), you can use neither of them.

Ember.Object is universal. Having this extra .content segment is a tiny price to pay for the straightforwardness it offers. I believe, ObjectProxy and ArrayProxy are the remnants of the bygone era of ObjectController and ArrayController.

These two addons approach promise wrapping on template level. They offer funky template APIs without offering anything that the described approach does not offer.

Compare these:

{{#if gitHubRateProxy.isRejected}}

  Failed to retrieve GitHub rate limit.<br>

  Reason: {{gitHubRateProxy.reason}}<br>

  <a href {{action 'refetchGitHubRate'}}>
    Retry
  </a>

{{else if gitHubRateProxy.isPending}}

  Retrieving GitHub rate limit...

{{else}}

  Your GitHub rate limit: {{gitHubRateRemaining}} ({{gitHubRatePercentage}})

{{/if}}
{{#deferred-content gitHubRatePromise as |d|}}
  {{#d.pending}}
    Retrieving GitHub rate limit...
  {{/d.pending}}

  {{#d.fulfilled as |gitHubRate|}}
    Your GitHub rate limit:

    {{gitHubRate.resources.core.remaining}}

    ({{multiply
      (divide gitHubRate.resources.core.remaining gitHubRate.resources.core.limit)
      100
    }}%)
  {{/d.fulfilled}}

  {{#d.rejected as |reason|}}
    Failed to retrieve GitHub rate limit.<br>

    Reason: {{reason}}<br>

    <a href {{action 'refetchGitHubRate'}}>
      Retry
    </a>
  {{/d.rejected}}
{{/deferred-content}}

Note how ember-deferred-content forces you to calculate percentage on the template level.

The main purpose of this article is to show you a pattern and make you give it a little thought. The pattern is fully legit and I'm using it whenever I don't feel like including ember-concurrency into my project.

There are at least two reasons to do this:

  • you care for your distribution size too much, and
  • you want to keep it simple and avoid extra layers of unnecessary abstraction and complexity

If you're already familiar with ember-concurrency and have it included in your project, there's no reason not to employ it for this use case. It may save you some typing:

gitHubRateTask: task(function * () {
  return yield this._fetchGitHubRate();
}).restartable().on('didInsertElement')

gitHubRate:          reads('gitHubRateTask.last.value'),
gitHubRateRemaining: reads('gitHubRate.resources.core.remaining'),
gitHubRateLimit:     reads('gitHubRate.resources.core.limit'),

// If we don't do this, the request will not be made. Smells imperative. :(
didInsertElement () {
  this._super()
  this.get('fetchGitHubRateTask').perform()
}

Here's the link to th full ember-concurrency example again.

Consider this use case. We're polling the backend every second and we want the last available result to be displayed at all times.

ember-concurrency offers access to the last resolution and rejection values even after the task has been restarted:

{{gitHubRateTask.lastSuccessful.value}}

If we do the same with the PromiseProxyMixin approach, the value on the page will be flashing every second. This is because the promise gets overwritten every second, and the previous resolution value becomes unavailable.

A quick solution would be to add .then(result => this.set('result', result)) to the promise, so that the resolved value gets extracted from the promise and stored separately.

This is a valid solution, but I don't like it for its imperativeness. Instead, consider this CP macro:

function cachingMacro (key) {
  let cache

  return computed(key, function () {
    const value = this.get(key)

    return value == null
      ? cache
      : cache = value
  })
}

It can be used like this:

gitHubRate:          cachingMacro('gitHubRateProxy.content'),
gitHubRateRemaining: reads('gitHubRate.resources.core.remaining'),
gitHubRateLimit:     reads('gitHubRate.resources.core.limit'),

Now, when the promise is overwritten with another promise that rejects, gitHubRate will still contain the resolved value of the first promise.

Or you can use ember-concurrency after all. 😬

Use the comments below to share your impressions, objections, and ideas. The most valuable part of an article is always the discussion that follows!