Our first task: an autocomplete searchbox

Transcript

In this video, we'll look at a naive implementation of an autocomplete searchbox, and use an Ember Concurrency Task to improve it.

Here's our current implementation:

// components/autocomplete-searchbox.js
import Ember from 'ember';

export default Ember.Component.extend({
  init() {
    this._super(...arguments);

    this.set('results', []);
  }),

  actions: {
    updateResults(query) {
      Ember.$.getJSON('/query', { query }).then(results => {
        this.set('results', results);
      });
    }
  }
});
// templates/components/autocomplete-searchbox.hbs
<p>Search:</p>
<input oninput={{action 'updateResults' value='target.value'}} type='text'>

<ul>
  {{#each results as |result|}}
    <li>{{result}}</li>
  {{/each}}
</ul>

The input triggers the updateResults action, which fires off a network request. When the server responds, the results list updates and the component rerenders.

We've hooked up a simple mock server that takes one second to respond, so we can see there's a small delay between when we type and when the list updates.

This component works, but it has a bug. To demonstrate it, let's add a button that shows and hides the component:

// templates/application.hbs
<button {{action (toggle 'showSearch' this)}}>Toggle search</button>

{{#if showSearch}}
  {{autocomplete-searchbox}}
{{/if}}

If we toggle the button and type into the searchbox, we see that everything still works.

Let's do it again, but this time, we'll enter text but then hide the searchbox before our server responds. We can see in the console that this causes our app to throw an exception: calling set on destroyed object. Clicking on the exception brings us to the problematic line in our component: our callback was trying to set an instance property on the component using this.set, but because of the toggle, the instance was already torn down.

This is exactly the kind of problem Ember Concurrency was designed to solve. Let's see how a Task can help us fix this.

First, we'll install the Ember Concurrency library:

ember install ember-concurrency

Next, we'll move the server query into a separate function:

// components/autocomplete-searchbox.js
actions: {
  updateResults(query) {
    this.queryServer(query);
  }
},

queryServer: function(query) {
  Ember.$.getJSON('/query', { query }).then(results => {
    this.set('results', results);
  });
}

Tasks are defined using the task helper, which can be imported from ember-concurrency. Tasks take a generator function as an argument, so we'll take our existing function, wrap it in task, and then make the function a generator. We do that by adding an asterisk after the function keyword:

import { task } from 'ember-concurrency';

queryServer: task(function * (query) {
  Ember.$.getJSON('/query', { query }).then(results => {
    this.set('results', results);
  });
})

Generators are a new ES6 language feature that allow us to start executing a function, but then pause it before it's finished. We'll see how to do that in just a moment. For now, it's enough to know that anything you can do inside a normal function, you can also do inside of a generator.

We've wrapped our queryServer function in a task, but there's one more change we need to make to get our code executing again. The task helper returns a special object, so we can't invoke it like a normal function. Instead, we'll need to get our task, and then call perform on it.

So, we'll change our updateResults action to look like this:

updateResults(query) {
  this.get('queryServer').perform(query);
}

Our code is working again - but we actually haven't fixed anything! Even though our queryServer function is now a Task, we have the same bug as before.

Now it's time for the fun part. The magic of Tasks lies in isolating the asynchronous parts of our code - the code that takes a long time to run. Looking at our queryServer task, which part of it takes a long time?

queryServer: task(function * (query) {
  Ember.$.getJSON('/query', { query }).then(results => {
    this.set('results', results);
  });
})

We can see it's the line that executes getJSON. getJSON is an asynchronous function that hits the network, and executes its callback at some point in the future.

To tell our Task that this part of the code is what takes time, we'll use the yield keyword. yield is actually a feature of generator functions, not the Ember Concurrency library; but it's a key part of what makes the library work. Ember Concurrency uses yield to decide whether it should continue execution of a task, or abort. In the next video we'll do a deep dive on generators and yield, but first, let's finish our task.

yield can feel a bit like magic. If we yield a promise, we get to work with the results on the next line, like this:

queryServer: task(function * (query) {
  let results = yield Ember.$.getJSON('/query', { query });

  // work with `results`
})

This looks like synchronous code - there's no callback! This is the beauty of generators and yield. It lets us write asynchronous code as if it were synchronous.

We can now move the code from our original callback and put it on the next line:

queryServer: task(function * (query) {
  let results = yield Ember.$.getJSON('/query', { query });

  this.set('results', results);
})

And we've actually just solved our bug! Let's see it in action.

The component works just like before, but this time if we hide the component before the request finishes, we don't see the exception thrown. This is because Ember Concurrency pauses at the yielded code, and only continues if the component is still on the page. When we toggle the button and the component is torn down, the task is canceled, and the last line simply never executes.

This is a shining example of the benefits of structured concurrency: we're able to write long-running code that is naturally bounded by its parent. Our programming model is aligned with our mental model, which makes it much easier to manage concurrent activities in our application.


We've now seen how Ember Concurrency helps us avoid bugs while writing clean, easy-to-read code. Next, let's learn about the awesome ES6 language feature that makes all of this possible: the generator function.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.