ES7 async/await with Aurelia

In this post we'll cover how the new async/await keywords can be used in conjunction with Aurelia's screen activation lifecycle.

In Aurelia you can opt-in to any of the screen activation lifecycle events by implementing the appropriate method on your viewmodel. If your method returns a promise, the router will wait for the promise to resolve before proceeding to the next step.

Loading data in activate

A common use-case for hooking into the screen activation lifecycle is to load data from a remote source to be displayed in the view. Let's take a look at a typical implementation for a listing of GitHub repos and their stars:

import {HttpClient} from 'aurelia-http-client';
import {inject} from 'aurelia-dependency-injection';

const reposUrl = 'https://api.github.com/orgs/aurelia/repos';

@inject(HttpClient)
export class App {
  repos = null;

  constructor(http) {
    this.http = http;
  }
  
  activate() {
    // return a Promise that will resolve when the repos have
    // been loaded and sorted by star count.
    return this.http.get(reposUrl)
      .then(response => {
        this.repos = response.content
          .sort((a, b) => b.stargazers_count - a.stargazers_count);
      });
  }
}
<template>
  <ul>
    <li repeat.for="repo of repos">
      <a href.bind="repo.html_url" target="_blank">
        ${repo.name} (${repo.stargazers_count} stars)
      </a>
    </li>
  </ul>
</template>

In this example Aurelia will wait for the Promise returned by the activate method to resolve before binding the view to the viewmodel.

Introducing async/await

With a little async/await sugar we can rewrite the activate method as if the GitHub API call was synchronous.

async activate() {
  let response = await this.http.get(reposUrl);
  this.repos = response.content
    .sort((a, b) => b.stargazers_count - a.stargazers_count);
}

The above code works seamlessly with Aurelia because the Babel transpiler will translate our async method to a method that returns a Promise, similar to our original implementation.

Async functions are currently at stage 2 (draft) in the TC39 proposal process. You may need to update the babelOptions in your config.js to enable this feature. Here's mine:

  ...
  babelOptions: {
    "stage": 2,
    "optional": []
  },
  ...

More info on the proposals implemented by babel here.

Multiple async calls

The GitHub API limits the number of results it will return in a single API call. The Aurelia GitHub organization has two pages of repos, so we actually need to make two API calls to grab all of them. Here's a typical implementation that chains the API call promises:

activate() {
  let response1;
  return this.http.get(`${reposUrl}?page=1`)
    .then(response => response1 = response)
    .then(() => this.http.get(`${reposUrl}?page=2`)
    .then(response2 => {
      this.repos = response1.content
        .concat(response2.content)
        .sort((a, b) => b.stargazers_count - a.stargazers_count);
    });
}

Here's what this would look like using async/await:

async activate() {
  let response1 = await this.http.get(`${reposUrl}?page=1`);
  let response2 = await this.http.get(`${reposUrl}?page=2`);
  this.repos = response1.content
    .concat(response2.content)
    .sort((a, b) => b.stargazers_count - a.stargazers_count);
}

Nicer with async/await right? The current implementation waits for the first API call to complete before executing the second. If we execute both API calls at the same time we can cut the activation time in half. Without async/await you'd do something like this:

activate() {
  return Promise.all([
    this.http.get(`${reposUrl}?page=1`),
    this.http.get(`${reposUrl}?page=2`)])
    .then(responses => {
      this.repos = responses[0].content
        .concat(response[1].content)
        .sort((a, b) => b.stargazers_count - a.stargazers_count);
    });
}

Rewritten with async/await you'd end up with this:

async activate() {
  let responses = await Promise.all([
    this.http.get(`${reposUrl}?page=1`),
    this.http.get(`${reposUrl}?page=2`)]);
  this.repos = responses[0].content
    .concat(responses[1].content)
    .sort((a, b) => b.stargazers_count - a.stargazers_count);
}

Try it out

As you can see, the new async/await keywords along with Promises can make async programming very manageable. Try async/await for yourself on the Aurelia plunker.