PromiseProxyMixin: pure Ember alternative to ember-concurrency

PromiseProxyMixin: pure Ember alternative to ember-concurrency

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

The most common use case though is simply fetching data from a resource. And in many projects, this use case can be the only reason to include ember-concurrency into the app.

The fact is that you don't need ember-concurrency for this! Ember has all the necessary parts included implementing this kind of data fetching with simplicity and efficiency while staying true to the Ember way.

Example use case

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

Why I have chosen this API endpoint is because it's the only one that GitHub doesn't limit, logically.

Let's implement a data fetching method:

import Controller from 'ember-controller';  
import fetch from "ember-network/fetch";

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

I'm using ember-network in a controller, but this can be anything that returns a promise, for example, the ember-ajax service.

Enter PromiseProxyMixin

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.

The mixin is not available as a module shim yet, so we'll have to take it from the Ember namespace.

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

import {default as EmberObject} from 'ember-object';

// Eventually this will be replaced with a direct import
import Ember from 'ember';  
const {PromiseProxyMixin} = Ember;

const PromiseObject = EmberObject.extend(PromiseProxyMixin);  

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

  // 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 PromiseObject.create({promise});
  }),

Using PromiseObject-driven computed property in a template

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:

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

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

  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}})  

How it works

When the template is rendered, the gitHubRateRemaining computed property reads gitHubRateProxy. The gitHubRateProxy in turn reads gitHubRatePromise. When the gitHubRatePromise computed property is first accessed, it calls the data fetching method and returns the promise. This promise is cached, so in all subsequent reads the computed property returns the same promise, implementing a pattern that ember-concurrency calls drop. When the promise resolves, its return value is wrapped into the PromiseObject and becomes available as gitHubRateProxy.content.

Accounting for a pending promise

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!

Accounting for a rejected promise

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}}

Demo

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


Keeping the logic on a service for reusability

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.

Ember.ObjectProxy is not necessary

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.

I'm not advocating against ember-concurrency, ember-deferred-content or ember-async-button

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 will save you lots of typing:

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

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

Also, the PromiseProxyMixin pattern is almost exactly what ember-deferred-content does under the hood. I'm not using it because I find its template pattern funky and I don't like passing a raw promise into my templates. If you're cool with these, ember-deferred-content will save you a few lines of code.

As for ember-async-button, its usage pattern used to be plain weird, but it became quite nice when they leveraged a closure action that can return a value. ember-async-button is a great tool when a button is all you need.

Tell me what you think

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

Seamless software development.

Code management and collaboration platform with Git, Subversion, and Mercurial.

Sign up for free
comments powered by Disqus