DEBUGGER: Promises, Async and React

Debugger is a series where I provide a problem and solution I come across in my work, in talking to others, or in my internet travels

Here's the situation. You've written a React component. You're going to update that component when you make an HTTP request. The function to make that call is written into a class for making such calls -- Transport, requestClient, something like that. Except, when you make that call, nothing happens. No dogs appear. The component stays the same.

You've tested to make sure you're hitting the endpoint correctly. You've seen this pattern followed before: it's a common pattern for authorization or just requests in general. Maybe your component and request function look a little something like this:

import React from "react";
import ReactDOM from "react-dom";

class AppSettings {
  constructor() {
    this.appSettings = {};
  }

  async getDog() {
    const url = await fetch("https://source.unsplash.com/1600x900/?dog").then(
      response => {
        if (response.status === 200) {
          return response.url;
        } else {
          console.log("aww heck..." + response.status);
        }
      }
    );

    this.appSettings = { dogURL: url };
  }
}

function App() {
  const appSettings = new AppSettings();

  return (
    <div className="App">
      <h1>Pupper Generator</h1>
      <button onClick={appSettings.getDog}>Get Dog</button>
      {appSettings.dogURL && (
        <img src={appSettings.dogURL} alt="Doggo, pupperino, or the like" />
      )}
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

No matter how hard you click that button, it doesn't get you a dog. Which, let's be honest, is terribly disappointing. Let's debug!

Console.experiment

My first step in debugging, assuming nothing jumps out at me, is to poke around with console.log. It's usually helpful to put multiple log statements around to save time and see where you get.

async getDog() {
  console.log("Activate!");
  const url = await fetch("https://source.unsplash.com/1600x900/?dog").then(
    response => {
      if (response.status === 200) {
        console.log(response.url);
        return response.url;
      } else {
        console.log("aww heck..." + response.status);
      }
    }
  );

  this.appSettings = { dogURL: url };
  console.log(this.appSettings);
}

Ok, so it looks like we got all the data we expected. Except, the component didn't re-render. What tells React to, well, react?

How React Reacts

One way to tell React to update is manual: you use ReactDOM's render method to explicitly replace a component in the UI. Why replace instead of update? React components are immutable, i.e. you can't change their children or attributes once created. Instead, you have to pass a whole new version of the component to the render method. You can see how this is done in the Getting Started tutorial.

But that's heavy handed for most apps. And what if you're making multiple calls or the user is interacting with the app? That's going to mean a lot of rerendering, not to mention keeping track of the changes. React is supposed to react on its own, right?

Right. React is optimized to handle the challenges of choosing how and when to update components depending on changes to their state. Except in this case we're not using React's state management. We've just added a variable to a class and are updating that. So we should use React to manage our state so we get rerenders on updates to the state.

Getting Stateful with Hooks

So where should the state live? We could keep it where it is, on the AppSettings class. That would mean we should probably make AppSettings extend React.Component and wrap our App in it. This is acceptable, though the React team has tried hard to make React cleaner to implement and easier to use by adding hooks. You can see the motivation here, but let's assume we want to go that route rather than create some kind of higher order component to wrap our App.

function App() {
  const [dogUrl, setDogUrl] = useState("");

Cool, so we're now using the useState hook to keep track of one piece of our component's state: the dog URL. We're using destructuring to pull out a variable that holds the state, in this case dogUrl, and a function to do the updating that will trigger the rerender we desire, setDogUrl. We just need a way to pipe the results of appSettings.getDog() to setDogUrl() and we should be onto something.

Let's write a little click handler to do just that!

function handleClick() {
  setDogUrl(appSettings.getDog)
}

<button onClick={handleClick}>Get Dog</button>

Well huh. We're getting an image placeholder and our alt text, but the doggo never appears. Oh right, we need to return the url now instead of just setting the class variable.

async getDog() {
  ...
  return url;
}

Well, that didn't seem to do it either. Let's try console.log in the handleClick function to see what we're getting.

function handleClick() {
  console.log(appSettings.getDog());
}

Hmm, getDog() seems to be returning a Promise instead of a URL. Why is that? Because async! We made getDog async so we could await that URL, and async functions return a promise by default. So all we need to do is make handleClick async and await the return of the getDog function.

async function handleClick() {
  setDogUrl(await appSettings.getDog());
}

Heck yes! Puppers! Now there's a bit more we can do to make this better. For one, we could tell the user the dog friend is coming with some kind of loader. And we could pull the getDog function out of the class since that's all we're using it for now. You can see the result of all those changes in this codepen

As always, if you have questions you can hit me up on twitter. Mahalo!