Previous: Post-Mortem
Next: Defining Tasks
Now we're going to build the same functionality using ember-concurrency tasks, starting with the same bare minimum implementation as before, and making incremental improvements.
For reference, here is the bare minimum implementation that we started with before (which only uses core Ember APIs):
Now let's build the same thing with ember-concurrency tasks:
Let's take a moment to point out everything that has changed:
First, instead of using a
findStores
action, we define a
findStores
task. This involves calling the
task()
builder function with
this
and modifying our async function to use the async arrow function syntax.
Second, in the template, instead of using
{{on "click" this.findStores}}
, we use
{{on "click" this.findStores.perform}}
.
Let's press onward with the refactor:
Rather than defining a separate boolean flag and manually tracking the state
of the task, we can use the
isRunning
property exposed by the task to drive our loading spinner, which means we only
need to make a change to the template code; the JavaScript can stay the same:
So far so good, but we still haven't addressed the issue that clicking the button multiple times causes weird behavior due to multiple fetch operations running at the same time.
Rather than putting an
if
guard at the start of the task, the ember-concurrency way to prevent
concurrency is to apply a
Task Modifier
to the task. The one we want to use is the
drop
modifier, which prevents concurrency by "dropping" any attempt to perform the
task while it is already running.
Now when you button mash "Find Nearby Stores", you no longer get the weird behavior due to concurrent fetches.
What about those pesky
"set on destroyed object"
errors?
Good news! Our code is already safe because ember-concurrency automatically
cancels tasks when their host object (e.g. a Component) is destroyed. In our
example, if the
findStores
task is paused at the unresolved
getNearbyStores
await
call right when the user navigates away, the component will be destroyed and
the
findStores
task will stop right where it is and will never hit the line of code with
this.result = result
, thus avoiding the
"set on destroyed object"
error.
Will a promise rejection/async exception put our task into an unrecoverable state?
It turns out that, again, we don't need to change any code; if either
getCoords
or
getNearbyStores
throw an exception, the
findStores
task would stop execution where the error occurred, bubble the exception to
the console (so that error reporters can catch it), but from there on the task
can be immediately performed / retried again. So, we don't need to change any
code.
JavaScript:
Template:
This was a very successful refactor. We were able to remove a lot of ugly boilerplate and defensive programming code, and what we're left with is very clean, concise, and safe.
Previous: Post-Mortem
Next: Defining Tasks