Run loop with setter in init after promise resolves


#1

This component handles a bunch of different shapes of arrays. plain array, promise, promise proxy, plain array with promises in it (which can be filtered). Some details hidden, but in init, it tries to render the collection (if plain array) as fast as possible (fastboot) and when the browser takes over then create gallery in didInsertElement (since using document.querySelector).

init() {
  // Because we may need to access a promisified object in the template, we need
  // to resolve before handing off
  this.get('promisifiedItems')
   .then(resolvedItems => this.set('resolvedItems', filterRejected(resolvedItems)))
   .catch(() => this.set('hasErrored', true))
   .finally(() => this.set('isDoneLoading', true));
},
 
didInsertElement() {
  // next only works, scheduleOnce is too early
  scheduleOnce('afterRender', this, () => {
    this.get('promisifiedItems').then(() => {
	  createGallery(); //  grabs each painted item and attaches css transformations
    });
  });
}

In a simple test I have, resolvedItems is set before didInsertElement but I can’t use scheduleOnce because resolvedItems has yet to paint. Want it to happen on a tight as schedule as possible so wanted to ask - is this expected (Ember 3.7) and is there some rules about the run loop, setters and painting to glean from this?


#2

Yes, this is expected behavior, component hooks are synchronous and will not understand async code. It is no wonder that scheduleOnce happens before next because scheduleOnce is still in the same call stack as the current execution. Unlike next which places the execution after everything has completed in the current execution loop.

However, this still represents a confusion in how this component is designed. If it were me I separate the promise resolution from the component itself. I would have a provider component manage the data and a gallery component for the actual rendering. For example I might have the template look like this:

{{#items-resolver items=this.items as |data|}}
  {{#if data.isPending}}
    Loading…
  {{else if data.isRejected}}
    Error: {{data.reason}}
  {{else}}
    {{items-gallery items=data}}
  {{/if}}
{{/items-resolver}}

In this way the item-resolver can judge who to best yield the data. Weather it be a direct pass-through in the case of a plain array or wrap it in a ArrayProxy.extend(PromiseProxyMixin) to gain the benefits of an array with derived state for the isPending/isRejected properties.

This also makes testing much easier. Your integration tests for items-gallery can now be fast and synchronous knowing that the application itself will handle promises at a higher level. It also means an integration test for the items-resolver does not require fully fleshed out gallery models/relationships and also removes the need to render a gallery just to test your promise resolutions. And in this way your acceptance tests can just be the happy path that the page rendered a gallery and not also if the gallery rendered correctly which speeds up tests significantly and lowers the necessary boilerplate/fixtures needed to accomplish a good set of tests.

Lastly, if you are trying to accomplish a mix (plain arrays with promises in them) you could have the items-resolver separate the data and/or have the gallery component know how to render a pending promise. Perhaps the items-resolver could covert the promise into PromiseProxyMixins and the gallery needs little logic to show a loading indicator for that relationship.

There are many ways to approach this problem. I think the take away is that you would benefit by separating the concerns to smaller modules/components instead of one component doing everything. And avoid performing async code from a components’ hooks but instead use a well known async method such as a PromiseProxyMixin or ember-concurrency task.


#3

@sukima This is brilliant. What a great recommendation. Thank you! One follow up question - would you say that the setter is placed on the current execution stack and the subsequent painting is on the next stack? For this one test case (albeit not representative) that is the debugging order I get with scheduleOnce- set, afterRender, then paint. next - set, paint, then next callback. Seems obvious from the fact that I can’t use scheudleOnce to grab the painted items. I would have assumed the async setter would NOT have been placed in the current execution stack.


#4

I would avoid setters all together and instead rely on computed properties offered by the provider compinent.