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!
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?
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.
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!