Previous: Older Version Docs
Next: Post-Mortem
To demonstrate the kinds of problems ember-concurrency is designed to solve, we'll first implement a basic example of loading data in a Component using only core Ember APIs. Then we'll introduce ember-concurrency tasks as part of a refactor.
This tutorial (and ember-concurrency itself) assumes that you have reasonable
familiarity with Ember's core APIs, particularly surrounding Components,
templates, actions, Promises, and the use of
async/await
.
For our use case, we're going to implement a Component that fetches and displays nearby retail stores. This involves a two-step asynchronous process:
This is basically the same example demonstrated in this EmberConf ember-concurrency talk; take a look if you prefer a video alternative to this tutorial. Please note, though, that the coding style has since changed with Ember Octane. The examples below have been updated to reflect the newer syntax.
We'll start off a bare-bones implementation of the feature: within an action
called
findStores
, we'll create an async function that fetches the
coordinates from a geolocation service and passes those coordinates to a
store's
getNearbyStores
method, which eventually gives us an array of stores that we stash on the
result
property so that the stores can be displayed in the template.
This first implementation works, but it's not really production-ready. The most immediate problem is that there's no loading UI; the user clicks the button and it seems like nothing is happening until the results come back.
We'd like to display a loading spinner while the code is fetching nearby
stores. In order to do this, we'll add an
isFindingStores
property to the component that the template can use to display a spinner.
We'll use
++
comments to highlight newly added code.
This is certainly an improvement, but strange things start to happen if you click the "Find Nearby Stores" button many times in a row.
The problem is that we're kicking off multiple concurrent attempts to fetch nearby locations, when really we just want only one fetch to be running at any given time.
We'd like to prevent another fetch from happening if one is already in
progress. To do this, just need to add a check to see if
isFindingStores
is true, and return early if so.
Now it is safe to tap the "Find Nearby Stores" button. Are we done?
Unfortunately, no. There's an important corner case we haven't addressed yet:
if the component is destroyed (because the user navigated to a different page)
while the fetch is running, our code will throw an Error with the message
"calling set on destroyed object"
.
You can actually verify that this happening by opening your browser's web inspector, clicking "Find Nearby Stores" from the example above, and then quickly clicking this link before the store results have come back.
The problem is that it's possible for our promise callback (the one that sets
result
and
isFindingStores
) to run after the component has been destroyed,
and Ember (and React and many others) will complain if you try and, well, call
set()
on a destroyed object.
Fortunately, Ember let's us check if an object has been destroyed via the
isDestroyed
flag, so we can just add a bit of defensive programming to our promise
callback as follows:
Now if you click "Find Nearby Stores" and navigate elsewhere, you won't see that pesky error.
Now, are we done?
You might have noticed that we don't have any error handling if either the
getCoords
or
getNearbyStores
await
calls throw an error.
Even if we were too lazy to build an error banner or popup to indicate that
something went wrong (and we are), the least we could do is make sure that our
code gracefully recovers from such an error and doesn't wind up in a bad
state. As it stands, if one of those
await
calls raises an error,
isFindingStores
would be stuck to
true
, and there'd be no way to try fetching again.
Let's use a
finally
block to make sure that
isFindingStores
always gets set to
false
, regardless of success or failure. Unfortunately, this also
means we have to duplicate our
isDestroyed
check.
And there you have it: a reasonably-production ready implementation of finding nearby stores.
Previous: Older Version Docs
Next: Post-Mortem