This page is part of a static HTML representation of TriTarget.org at https://tritarget.org

Delayed loading indicators with ember-concurrency

Sukima6th February 2019 at 9:01pm

I ran into a interesting puzzle over the weekend. I was using ember-concurrency's derived state to show a loading indicator. This seems perfectly reasonable:

import Ember from 'ember';
import { task } from 'ember-concurrency';

export default Ember.Component.extend({
  showLoadingIndicator: Ember.computed.reads('myTask.isRunning'),

  myTask: task(function * () {
    let result = yield ajaxRequest('http://example.com/resource');
    return result;
  })
});

Then in my template I simply use myTask.isRunning to show a loading indicator. Normally this would work except in my case the asynchronous task I was doing was preloading an image. It was so fast that the loading indicator would show but for a fraction of a second causing a blip or flicker.

I instead had to introduce an artificial delay into the task so that the loading indicator would show only if the resolution took longer then a set amount of time. I used race to test if the result resolved first before a timeout() did.

import Ember from 'ember';
import { task, timeout, race } from 'ember-concurrency';

export default Ember.Component.extend({
  showLoadingIndicator: false,

  myTask: task(function * () {
    let promise = ajaxRequest('http://example.com/resource'); // [1]
    try {
      let result = yield race([promise, timeout(250)]);       // [2]
      this.set('showLoadingIndicator', true);                 // [3]
      return yield promise;                                   // [4]
    } finally {
      this.set('showLoadingIndicator', false);                // [5]
    }
  })
});
  1. First store the promise into a variable because we need it in more than one place.
  2. Here we race the resolution of promise against a promise that resolves after 250ms.
  3. Regardless of who wins we set the loading indicator to true. (Yes this works.)
  4. Wait for the ajaxRequest value and return that.
  5. With the finally block we guarantee that when the ajaxRequest resolves the loading indicator will be hidden. This happens even when the request fails and throws an exception.

I at first thought that I had to wrap the this.set('showLoadingIndicator', true); in a conditional so that it would only execute if the result of the race was because the timeout resolved first (resolving as undefined). Turns out you don't need it. If the timeout finishes first then the yield promise will still be waiting while the showLoadingIndicator is true. However, if the ajaxRequest promise resolves first setting showLoadingIndicator to true is kind of a no-op because it will be set to false immediately afterwords (in the same Ember run loop) because the yield promise will not have to wait. And since rendering happens at the end of the Ember Run Loop the fact that showLoadingIndicator was set to true is lost the moment it gets set back to false.

I've developed a working example of this in an ember-twiddle: https://ember-twiddle.com/18baf8b630d691bcf0d757d68f70cff2

Discuss this article