RFC: Reset derived state
See original GitHub issueSummary
In this issue, I would like to propose public API to reset the derived state
for a task. Calling the function would cause the last*
task object properties
to reset to undefined
, allowing interface that depends on these properties to
“start over”.
Motivation
The motivation for this change is best described with an example. I know I’ve looked for the existence of this functionality in the documentation for other use-cases. I have chosen this use case as my example as it is what I am working on right now and is therefore freshest.
I have a form, where at first there is one dropdown element, and when an item from the first dropdown is selected, an ajax request is fired to load options for a second element. In my current case, there is also a third dropdown, whose options get loaded when an item is selected from the second dropdown.
The following will use country, region, and city, as an arbitrary and hopefully
understandable example. Below are a component template and implementation to
illustrate how I accomplish this now. Derived state is reset by writing the
task to return undefined
, thereby unsetting the value from lastSuccessful
.
Template
{{ember-power-select options=countries onChange=(action 'selectCountry')}}
{{! Not shown for simplicity: a loading indicator when a task isRunning }}
{{#if regions}}
{{ember-power-select options=regions onChange=(action 'selectRegion')}}
{{/if}}
{{#if cities}}
{{ember-power-select options=cities onChange=(action (mut 'city'))}}
{{/if}}
Component
import Component from '@ember/component';
import { readOnly } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default Component.extend({
countries: [...],
regions: readOnly('loadRegions.lastSuccessful.value'),
cities: readOnly('loadCities.lastSuccessful.value'),
loadRegions: task(function*(country) {
return yield ajax(...);
}),
loadCities: task(function*(region) {
// When called with an explicitly falsy value, return undefined to reset
// derived state.
if (!region) { return; }
return yield ajax(...);
}),
actions: {
selectCountry(country) {
this.set('country', country);
// If a region and city have been selected, and the country is changed,
// we need to reset the state of the cities, or the cities dropdown
// will still be populated with options from the previously selected
// country. This explicitly resets the derived state for cities.
this.get('loadCities').cancelAll();
this.get('loadCities').perform(null);
this.get('loadRegions').perform(country);
},
selectRegion(region) {
this.set('region', region);
this.get('loadCities').perform(region);
},
},
});
This approach works, but doesn’t feel idomatic and requires awkward special case code in the task body itself.
Detailed design
I would hope it would be possible to expose something like a reset()
function
on the task property, so that, given the previous example, it would be possible
to call this.get('loadCities').reset()
instead of
this.get('loadCities').perform(null)
.
Component re-implementation
import Component from '@ember/component';
import { readOnly } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default Component.extend({
countries: [...],
regions: readOnly('loadRegions.lastSuccessful.value'),
cities: readOnly('loadCities.lastSuccessful.value'),
loadRegions: task(function*(country) {
return yield ajax(...);
}),
loadCities: task(function*(region) {
return yield ajax(...);
}),
actions: {
selectCountry(country) {
this.set('country', country);
// If a region and city have been selected, and the country is changed,
// we need to reset the state of the cities, or the cities dropdown
// will still be populated with options from the previously selected
// country. This explicitly resets the derived state for cities.
this.get('loadCities').reset();
this.get('loadRegions').perform(country);
},
selectRegion(region) {
this.set('region', region);
this.get('loadCities').perform(region);
},
},
});
I don’t know enough of the ember-concurrency internals to know how best to
implement this. Would it be possible to schedule an empty task. This could
either be explicitly scheduling null
or a null-object TaskInstance
?
What happens if there are currently running tasks?
I see two options for how reset()
would work, and I’m not sure which would be
better, although I would learn toward the first option here as it represents my
use-case the best.
-
It might make sense for resetting to
cancelAll
and reset derived stateIn this case, it might make more sense to reset the entire scheduler, although I’m not sure what other implications that might have.
-
Another option, and my assumption is that this makes the most sense, is that reseting is equivalent to scheduling a new empty task
This means the new empty task would follow the normal concurrency rules. I think this might make sense for enqueue, restartable, and keepLatest, but might not make sense with drop.
How we teach this
The new function could be documented in the API documentation, and I think it
would make sense to add a reset
button to the examples in the Derived State
documentation.
Drawbacks
One drawback is adding to the API surface area. Another drawback is potential confusion about how it works, especially depending on what approach is taken about how to handle currently running tasks.
Alternatives
The obvious alternative is to not add this functionality, as there is a workaround.
Unresolved questions
My biggest question is already outlined above in “What happens if there are currently running tasks?”.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:2
- Comments:9 (6 by maintainers)
Top GitHub Comments
UPDATE: Check out #253, which works for me in manual testing on my application. If you mentioned this would be useful, would you give it a shot by referencing my branch in
package.json
and trying it out?Also I think we can safely change the first string arg to cancelAll into a hash of options:
I snuck in the reason string arg as private API and it hasn’t been documented; I highly doubt the change would break anyone’s code.