Skip to content

Fetching on Mount

In the previous lesson, we saw how to make a network request in response to an event, like a form submission. But what if we need to fetch data to populate the initial view?

For example, let's suppose we're building a weather app. We want to show the user what the current temperature is in their area:

Screenshot showing that the current temperature is 15 degrees celsius

We want to show the temperature immediately, right when the component mounts.

This is a surprisingly thorny problem. It might seem like a slight difference, fetching on mount instead of fetching on event, but it opens a whole can of worms.

First, the ergonomics are tricky. In order to fetch on mount, we'd generally use the useEffect hook, but there are some gotchas around using async/await in an effect (covered in an upcoming lesson).

And also, if we want to solve this problem in a robust, production-ready way, there are all sorts of concerns we need to worry about, including:

  • Caching the response so that it can be reused by multiple components across the app.
  • Revalidating the data so that it never becomes too stale.

This is a rabbit hole we could get lost in for days. 😬

As a result, it's become standard in the community to use a tool to help with this stuff. The React docs, for example, suggest using a package like React Query or SWR. In fact, on this course platform, I use SWR to solve all of these hard problems for me!

Screenshot of the SWR homepage

Let's see how it works.

Intro to SWR

Here's an implementation. Feel free to poke and prod at it. We'll go over how it works below.

Code Playground

import React from 'react';
import useSWR from 'swr';

const ENDPOINT =
'https://jor-test-api.vercel.app/api/get-temperature';

async function fetcher(endpoint) {
const response = await fetch(endpoint);
const json = await response.json();
return json;
}

function App() {
const { data, error } = useSWR(ENDPOINT, fetcher);
console.log(data, error);
return (
<p>
Current temperature:
{typeof data?.temperature === 'number' && (
<span className="temperature">
{data.temperature}°C
</span>
)}
</p>
);
}

export default App;
preview
console
  1. undefined
    undefined
  2. undefined
    undefined
  3. {
    "ok": true,
    "temperature": -1
    }
    undefined
  4. {
    "ok": true,
    "temperature": -1
    }
    undefined

Let's dig into this code:

Video Summary

On line 2, we import the useSWR function from the library:

import useSWR from 'swr';

This is a custom hook. In my opinion, this is super cool. We can essentially "borrow" their solution, and implement it in a single line of code, benefitting from the hundreds/thousands of hours the team has spent developing, battle-testing, and iterating on it.

We use it like this:

const { data, error } = useSWR(ENDPOINT, fetcher);

The useSWR hook requires two things:

  1. A unique key, typically the endpoint you'd like to make a request to
  2. A fetcher function

People think that libraries like SWR actually make the network requests for us, but this is a misconception. No matter whether we use a library like this or not, we're still the ones writing the Fetch calls by hand. The library is agnostic when it comes to data-fetching. SWR decides when to run the request, and what to do with the result.

Here's the fetcher function:

async function fetcher(endpoint) {
const response = await fetch(endpoint);
const json = await response.json();
return json;
}

Whatever we return from our fetcher function will be set as the data variable inside useSWR. In this case, json is equal to:

{
"ok": true,
"temperature": -19
}

(The "temperature" is a random value.)

In our App component, we pluck data and error from useSWR, and we use these variables in the rendering. If data exists, it means the request has resolved, and we can use it in our UI, to show the temperature.

When useSWR calls our fetcher function, it provides the key as the 1st parameter. This might seem silly in this example, when we can use the ENDPOINT constant directly, but the idea is that fetcher functions are meant to be generic. I can define a single fetcher function in a “helpers.js” file and use it for every on-mount network request in the app.

Now, this probably seems like a lot of trouble. If we're still the ones making the network request, what benefit does the tool provide?

One benefit is the “revalidation”. It's right in the name; SWR stands for Stale While Revalidate.

The network request will be made immediately on mount, to fetch the original value, but it will be repeated at certain strategic times, so that the value never grows too stale.

For example, suppose we switch tabs, and we spend some time on another tab. When the user returns, useSWR will automatically repeat the network request, to fetch a fresh value, so that the user sees the latest temperature.

This is known as revalidating. If the value has changed, it'll immediately replace the old value.

While that request is pending, however, we continue to show the old value. The stale value is shown while revalidating.

This behaviour is customizable, so we can tweak it to our exact needs. There are other benefits as well (eg. caching).

Loading and error states

Our MVP above doesn't include loading or error states. Let's see how to implement them with SWR.

Loading

In addition to providing a data value, the useSWR hook also tells us whether or not the request is currently loading. We can pluck out the isLoading key:

const { data, isLoading } = useSWR(ENDPOINT, fetcher);

isLoading is a boolean value. The initial value is true, and it flips to false once the fetch request has completed.

We can use that value to conditionally render a loading UI like this:

function App() {
const { data, isLoading } = useSWR(ENDPOINT, fetcher);
if (isLoading) {
return (
<p>Loading…</p>
);
}
return (
<p>
Current temperature:
{typeof data.temperature === 'number' && (
<span className="temperature">
{data.temperature}°C
</span>
)}
</p>
);
}

Error

To simulate an error, we can add a query parameter to the ENDPOINT:

const ENDPOINT =
'https://jor-test-api.vercel.app/api/get-temperature?simulatedError=true';

This will cause the server to return a 500 status code 👀, instead of 200. It will also return the following JSON:

{
"error": "This request returns an error, because the “simulatedError” query parameter was specified."
}

With SWR, however, neither of these things is sufficient to mark this as an error.

Remember: we manage the network request! Our fetcher function is responsible for retrieving the data, and passing it along to SWR. If we want this to count as an error, we need to throw it:

async function fetcher(endpoint) {
const response = await fetch(endpoint);
const json = await response.json();
if (!json.ok) {
throw json;
}
return json;
}

With this change done, data will be undefined, and error will be the object we got back from the server.

Here's the final implementation, with loading and error states:

Code Playground

import React from 'react';
import useSWR from 'swr';

// Remove "?simulatedError=true" to see the success state:
const ENDPOINT =
'https://jor-test-api.vercel.app/api/get-temperature?simulatedError=true';

async function fetcher(endpoint) {
const response = await fetch(endpoint);
const json = await response.json();

if (!json.ok) {
throw json;
}

return json;
}

function App() {
const { data, isLoading, error } = useSWR(ENDPOINT, fetcher);

if (isLoading) {
return <p>Loading…</p>;
}

if (error) {
return <p>Something's gone wrong</p>;
}

return (
<p>
Current temperature:
{typeof data?.temperature === 'number' && (
<span className="temperature">
{data.temperature}°C
</span>
)}
</p>
);
}

export default App;
preview
console