Coding Challenge: Async/Await and React

by Alec Barlow

Disclaimer: This article assumes the reader has some understanding of JavaScript, Promises, and React.

When I first started learning JavaScript, the hardest concept for me to understand was the non-blocking, asynchronous nature of certain blocks of code. I had just spent the previous six months learning Python, and in addition to reading books, line by line, for most of my life, I thought I had a pretty good understanding of how code runs. In simplified terms, a line of code is evaluated, once complete, the line below it is then evaluated. But after spending some time with JavaScript, and a few grey hairs later, I realized that my previous assumptions were in need of garbage collection.

How JavaScript Works

Blocking vs. Non-Blocking Code

Let compare two snippets of code, one in Python, the other in JavaScript. Both blocks will output the same thing (sort of).

    
      # Python 3 (Blocking)
      
      import requests
      
      def getUsers():
        response = requests.get('https://jsonplaceholder.typicode.com/users')
        fetchedUsers = response.json()
        return fetchedUsers
      
      users = getUsers()
      print(users) # prints [{'id': 1, 'name': 'Leanne Graham'...}]
    
  
    
      // JavaScript (Non-Blocking)
      
      const getUsers = () => {
        return fetch('https://jsonplaceholder.typicode.com/users')
        .then(response => response.json())
      }
      
      let users;
      getUsers()
      .then(fetchedUsers => {
        users = fetchedUsers;
        console.log(users); // logs [{'id': 1, 'name': 'Leanne Graham'...}]
      });
      console.log(users); 
      // users is undefined...
      // Can we even assign the data from the request to it?
      // Sounds like the beginnings of 'callback hell'.
    
  

Do I need to ask which snippet is easier to read and follow along?

Now, this article is not about the ins and outs of managing synchronous vs. asynchronous code. It is not a rant about JavaScript, either. The purpose of this article is to teach you a new syntax pattern, using interactive examples, that will drastically improve the readability of your code, because writing code that can be understood by other humans is arguably the most important part of being a programmer.

Async/Await

The async function and await operator were initially defined in ES2017. They are supported in Node 7.6 and above and in the create-react-app boilerplate package. In the following CodePen examples, you will see how these keywords can be used to clean up the code of a React component. The code in each example is fully editable, and for some of them, implementing the solution will be up to you. The app used is the same for each example; it makes a request for some fake user data, then renders cards for each user. In addition, each solution only changes the componentDidMount method of UserList, which starts on line 16 of each Pen's Babel tab.

Also, CodePen does not currently support async/await by default. I had to add this Javascript resource: https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.29/browser-polyfill.min.js

Example #1: The basics

Starting on line 9, you will notice that this component currently uses Promise based syntax to obtain user data. Switching to async/await syntax will make the logic in componentWillMount much easier to understand. To do that, comment out lines 9-14 and add this:

    
     async componentDidMount() {
       let response = await api.users.get();
       this.setState({users: response.data, apiError: null});
     }
    
  

See the Pen a/a ex 1 by Alec Barlow (@abarlow85) on CodePen.

Solution #1

Let's go through the solution line by line.

In the method declaration, async componentDidMount(), using async defines an asynchronous function. This does two things. First, when the method is called, it now returns a Promise. Second, the method can now contain the await expression.

The next line, let response = await api.users.get();, uses this expression. Using await pauses the execution of the method until the Promise to the right of it is resolved. And here is the awesome part, the resolved value (the response object) is assigned to our response variable.

Lastly, although nothing changed with this.setState({users: response.data, apiError: null}), accessing response.data would not be possible without using the async/await syntax. And to summarize, the entire methods now looks synchronous to the human eye, with each line completing before the one below it runs.

Example #2: Handling errors

Handling errors is also straight forward. Simply put, instead of .then and .catch blocks of code, which is used in Promise based syntax, a try/catch block is used. Using the following template, comment out the componentDidMount method in the Pen below and write it again using async/await. The only lines that you need to worry about are 9-17.

  
    // solution template
    async componentDidMount() {
      try {
        // wait for the request to finish
        // then update state
      } catch (err) {
        // do something with the err
        // how about displaying it in the browser?
        this.setState({users: [], apiError: err.message});
              
        // to test, you can create an error by 
        // changing ROOL_URL on line 89
      }
    }
  

See the Pen YQGjRM by Alec Barlow (@abarlow85) on CodePen.

Solution #2

How did it go? Here is the answer that I came up with.

  
    async componentDidMount() {
      let response, error;
      try {
      
        response = await api.users.get();
        this.setState({users: response.data, apiError: null});
        
      } catch (err) {
      
        error = err;
        this.setState({users: [], apiError: err.message});
        
        // test by changing ROOT_URL on line 89
      }
    }
    
  

The first thing I want to point out is that I initialized response and error outside of the try/catch block. This is a useful practice because it means we can access the response or err objects outside of try/catch, and it will also help us avoid a ReferenceError because a variable is not defined.

Second, the reason a try/catch block is used has to do with the await operator. When the Promise to the right of await resolves (resolve being the keyword), the resolved value gets assigned to our response variable. However, if the Promise is rejected, our code throws an error, and that error is automatically passed to the catch block.

Finally, our code is easier to read because we no longer have to worry about nesting callbacks (and the parameters they call when invoked) inside .then and .catch. It's time for something more difficult...

Example #3: Nested API calls (Advanced)

So far, we have only been dealing with a single network request for data. But, what if we had to take the data from the first request and make a separate request for each data point. Sounds like a "real-world" example to me; using data from one request, get additional data using subsequent requests. For this example, using the array of users that we obtained from the first request, we are going to make additional requests in order to populate each user's card with his or her todo list.

The challenge I have for you is to rewrite some methods in the Pen below to use async/await syntax instead of Promise based syntax. There are only two methods you need to deal with, componentDidMount on lines 9 to 24 and mapTodosToUser on lines 26 to 37. No templates this time, sorry, but here is a review of the concepts behind async/await:

GOOD LUCK!

See the Pen a/a ex 3 by Alec Barlow (@abarlow85) on CodePen.

Solution #3

Hopefully, I didn't lose you. Here is my solution:

  
  async componentDidMount() {
    let response, error, users;
    try {
      response = await api.users.get();
      
      // Remember that .map returns an array of Promises.
      // await Promise.all() will wait for
      // all of them to resolve (or be rejected)
      users = await Promise.all(response.data.map(this.mapTodosToUser));
      
      this.setState({users, apiError: null});

    } catch (err) {
    
      error = err;
      this.setState({users: [], apiError: err.message});
      
    }
  }
  
  async mapTodosToUser(user) {
    let response, error;
    try {
      
      response = await api.users.todosForUser(user.id);
      user.todos = response.data;
      
    } catch (err) {
    
      error = err;
      user.todos = [{error:`Unable to get todos: ${err.message}`}];
      
    }
    
    return user;
  }
  

By now, you might be noticing a pattern when converting between Promise based syntax and async/await. Async/await is a wrapper around Promises, with the added benefits of having more control over your code flow, and, most importantly, having code that can be read synchronously to the human eye.

Start using async/await now, and enjoy not having to worry about Promise chaining and "callback hell" anymore!

Previous Post Next Post