OK, first some background:
Background
The basic concept is that in many places in any application, you have components that:
- Accept one or more parameters – parameters which are changed by the user (i.e., not static)
- Load some data – possibly one set of data, possibly multiple sets of data
- Do some transformation on the data
- Display (i.e., render) the result
(In short, a pretty standard/normal workflow.)
It’s not hard to do this programatically – for example:
- An observer on all your parameters…
- …which calls a ‘loadData’ method…
- …with
yield
s to handle async…
- …then transforms the data…
- …then sets the data into a property (or properties) which are rendered.
Of course, you inevitably find that the real life case requires a few twists, and then you have to deal with live updates to your data coming in via MQ, users aborting in midstream (though ember-concurrency helps a log), etc…
Ember, on the other hand, really encourages a ‘declarative’ approach to this type of operation – a cascading set of computed properties that define what you want instead of how to go about the operation of getting it.
Once you buy into this ember concept, you find things becoming a LOT simpler, and a million times more maintainable.
Ember even provides things like PromiseArray
and PromiseObject
to handle async operations inside computed properties (after all, the entire point of computed properties is that you don’t have to care about the order of operations or waiting on the results – if things load a bit later, your template simply updates with the new data and redraws what is needed).
Where it really becomes beautiful, though, is when you use tasks – tasks automatically take care of all of the cancellation/cleanup for you, and they expose awesome ‘derived state’ properties! This means that the complexity of using PromiseArray
or PromiseObject
completely goes away – you simply reference the task.value
property, which is null
or undefined
initially, and gets filled in when the task is done!
So how am I trying to do this in real life?
Example
Let’s assume that I have a simple component that displays invoices for a given customer and month. That component then could look something like this:
export default Ember.Component.extend({
month: null, // These parameters will be set externally -- presumably
customer: null, // we'll have some sort of dropdown/picker.
invoiceDataTask: task(function * (month,customer) {
return yield this.get('store').query('invoice',...month & customer parameters...);
}),
invoiceDataTaskInstance: Ember.computed('month','customer',function() {
const month = this.get('month');
const customer = this.get('customer');
return this.get('invoiceDataTask').perform(month,customer);
}),
invoiceData: Ember.computed.reads('invoiceDataTaskInstance.value'),
isLoading: Ember.computed.reads('invoiceDataTask.isRunning')
});
and we have a functionally complete component js file! As you can see:
- Any time the user changes the month or customer, the data will automatically reload (the task will be refired).
- The data shows up in the
invoiceData
property as soon as the load is finished.
- I can immediately tell whether or not my data has loaded, by simply checking
isLoading
In short, a very simple, declarative way of expressing this.
My hbs file would then look something like:
<div class="{{if isLoading 'loading' 'loaded'}}">
{{#each invoiceData as |invoice|}}
...all sorts of lovely invoice display stuff...
{{/each}}
</div>
Trouble in Paradise
Where the problem crops up is with the isLoading
declaration – what happens is:
- Ember chooses to compute this property first, and it returns ‘false’ since no task is (yet) running.
- Ember then chooses to compute the
invoiceDataTaskInstance
, which fires off the task
- This then triggers a recompute of
isLoading
, because ember-concurrency has incremented the ‘number of instances running’ variable.
- Recomputing
isLoading
triggers the infamous ember ‘You modified “isLoading” twice in a single render.’ error, because the template makes use of this flag.
Solutions
I went through a number of different solutions, and my almost-last one was the observer thing that I tried – this worked, but was (as you say) a bit complicated.
However, in looking at the problem again, it seemed to me that the true crux of the issue was the fact that the invoiceDataTaskInstance
property was the one firing off the task (and setting the isRunning
, but the CHECK for isRunning
goes only against invoiceDataTask
(a subtle difference). This is why ember is computing the ‘isLoading’ property first – all of its dependencies are satisfied, and it doesn’t need invoiceDataTaskInstance
.
Modifying my declaration from:
isLoading: Ember.computed.reads('invoiceDataTask.isRunning')
to:
isLoading: Ember.computed('invoiceDataTaskInstance','invoiceDataTask.isRunning',function() {
this.get('invoiceDataTaskInstance'); // If you don't "get" a property that you declared in your deps, it won't really be a dep.
return this.get('invoiceDataTask.isRunning');
})
then solved the problem – Ember now renders things in correct dependency order.
I’m not 100% positive I won’t still (in some way) be able to create a case in which this occurs again, but initial testing is positive.
Thanks for all the comments and help!