Peter Ekene Eze Learn, Apply, Share

Handling data fetching in Next.js with useSWR

7 min read 2215

Handling Data Fetching in Next.js with useSWR

Data fetching patterns are a very important part of every web framework. That is why this part of every web technology has constantly seen improvements and innovations.

Seeing that modern web development paradigms rely a lot on data-fetching functionalities to support features like SSR and CSR, it makes sense to stay up to date with changes in this area of the web.

In this post, I’ll introduce you to the useSWR hook that was just recently introduced to Next.js to help make data fetching easier. To do that, we’ll build a random user generation site. Not that you need yet another one of these random generation sites, but it has proven to be effective in showing developers how things work.

My goal is to ensure that at the end of this post, you’ll know more about the useSWR hook and consequently improve your Next.js authoring experience with it. Before we dive in, here’s a brief about it.

useSWR

SWR in this context stands for “stale-while-revalidate,” which is a term I imagine Next.js developers are already familiar with. The Next.js team built it to give developers more ways to fetch remote data when working with Next. It is basically a set of React Hooks that provide features like revalidation, mutation, caching, etc. out of the box.

I like to think of it this way: the problem useSWR solves for me is that it gives me the opportunity to show something to users immediately, and a convenient way to manage their experience while the actual content gets loaded behind the scenes. And it does it likes this:

  • When a request is made, it first returns a cached value. Probably something generated in the getStaticProps() function
  • Next, the server will start a revalidation process and fetch the actual data for the page
  • Finally, when revalidation is successful, SWR will update the page with actual data

When this is the case, users won’t be stuck looking at loading screens and your site remains fast and performant.

useSWR with TypeScript

useSWR ships with TypeScript support out of the box. It also has inferred types for the arguments passed into fetcher to give us types automatically. That said, you can define custom types for your own arguments in the simplest way possible like so:

import useSWR from 'swr'
const { data } = useSWR('https://www.users.com', (apiURL: string) => fetch(apiURL).then(res => res.json())

There are no special TypeScript specific configurations needed to work with SWR. You can read more about TypeScript with SWR here.

Amongst other benefits of SWR, you can also use it to retrieve data from any HTTP-supported servers.

Let’s dive in and set up our random user generator app with Next.js and the useSWR hook.

Setting up a Next.js application

To quickly set up a Next.js application, open a terminal window and run the create-next-app command like so:

npx create-next-app useswr-user-generator

Follow the prompts to complete the setup process and you should have a useswr-user-generator app locally. Navigate into the application directory and install SWR with this command:

cd useswr-user-generator # navigate into the project directory
npm install swr axios # install swr and axios
npm run dev # run the dev server

The commands above will install both the SWR and Axios packages and open the project on your browser at localhost:3000. If you check, we should have the project live on that port like so:

Welcome to Next.js Page

Great, we’ve successfully set up a Next.js application. Let’s go ahead and build this random generator thing, shall we?

Data fetching with SWR

In the project’s root, create a components folder. Inside this folder, add a Users.js file and update it with the snippet below:

// components/Users.js

import axios from "axios";
import useSWR from "swr";
import Image from "next/image";

export default function Users() {
  const address = `https://randomuser.me/api/?results=6`;
  const fetcher = async (url) => await axios.get(url).then((res) => res.data);
  const { data, error } = useSWR(address, fetcher);

  if (error) <p>Loading failed...</p>;
  if (!data) <h1>Loading...</h1>;

  return (
    <div>
      <div className="container">
        {data &&
          data.results.map((item) => (
            <div key={item.cell} className={`user-card  ${item.gender}`}>
              <div>
                <Image
                  width={100}
                  height={100}
                  src={item.picture.large}
                  alt="user-avatar"
                  className="img"
                />
                <h3>{`${item.name.first}  ${item.name.last}`}</h3>
              </div>
              <div className="details">
                <p>Country: {item.location.country}</p>
                <p>State: {item.location.state}</p>
                <p>Email: {item.email}</p>
                <p>Phone: {item.phone}</p>
                <p>Age: {item.dob.age}</p>
              </div>
            </div>
          ))}
      </div>
    </div>
  );
}

Let’s walk through this code. In the snippet above, we are using the useSWR() hook to fetch the data for six users as specified in our API address variable.

The useSWR hook accepts two arguments and returns two values (based on the status of the request). It accepts:

  • A key — a string that serves as the unique identifier for the data we are fetching. This is usually the API URL we are calling
  • A fetcher — any asynchronous function that returns the fetched data

It returns:

  • data — the result of the request (if it was successful)
  • error — the error that occurred (if there was an error)

At its simplest state, this is how to fetch data with useSWR:

import useSWR from 'swr'

export defaul function Users() {
  const { data, error } = useSWR('/api/users', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

In our case, we saved our API URL in the address variable and defined our fetcher function to process the request and return the response.



N.B., the useSWR docs suggest that using the Next.js native fetch function would work with SWR, but that wasn’t the case for me. I tried it but it didn’t quite work, hence why I’m using Axios instead.

Let’s check back on the browser and see if we get our users. To show the users page, let’s import the <Users /> component into the pages/index.js file and update it like so:

// pages/index

import Head from "next/head";
import Users from "../components/Users";

export default function Home() {
  return (
    <div>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Random user generator" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Users />

    </div>
  );
}

You might have noticed that I’m using the Next.js image component to show the user avatars in the Users.js file. As a result, we need to specify the image domains in the next.config.js file like this:

module.exports = {
  reactStrictMode: true,
  images: {
    domains: ["randomuser.me"],
  },
};

Now when you check back on the browser, we should see our users showing up as expected:

Random User Generator Next.js

There! We have our users. So, our useSWR hook is working! But is that it? How is this any different from other data fetching methods? Read on…

Why useSWR?

It’s a likely question to ask at this point. Other than being very declarative, I wouldn’t say this is a huge improvement for me yet. Which probably makes now a good time to talk about a feature of useSWR I really like (among others):

Pagination

Pagination with useSWR is a breeze. Let’s exemplify it: imagine that instead of loading just six users, we want the ability to generate more users and add them to this page on demand. This is particularly helpful when you’re building an application where users need to navigate through multiple pages of the same content. We can demonstrate this by adding a Load More button at the end of our users page to generate more users when clicked. Let’s update the index page to pass a count prop to our <Users /> component:

// pages/index.js

import Head from "next/head";
import Users from "../components/Users";
export default function Home() {
  const [count, setCount ] = useState(0);
  return (
    <div>
      <Users count={count} setCount={setCount} />
    </div>
  );
}

Next, let’s add a button in the components/Users.js file to update the count and load more users to the page from our API:

import axios from "axios";
import useSWR from "swr";
import Image from "next/image";

export default function Users({ count, setCount }) {
  const address = `https://randomuser.me/api/?results=${count}`;
  const fetcher = async (url) => await axios.get(url).then((res) => res.data);
  const { data, error } = useSWR(address, fetcher);
  if (error) <p>Loading failed...</p>;
  if (!data) <h1>Loading...</h1>;
  return (
    <div>
      <div className="container">
      // show users 
      </div>
      <center>
        <div className="btn">
          <button onClick={() => setCount(count + 3)}>Load More Users</button>
        </div>
      </center>
    </div>
  );
}

Let’s check back on the browser and see if our button is working as expected. When clicked, it should load three more users and render them on the page along with our existing users:

Works like a charm. However, this introduced another problem for us. When the button is clicked, Next.js will make a fresh request to our API to fetch those three more users, which will trigger our loading state. That’s why you notice a flicker on the screen when the button is clicked. Luckily, we have a simple solution for this, much thanks to the SWR cache.

We can pre-generate those users (or a next page in a different context) and render them in a hidden <div> element. This way, we’ll just display that element when the button is clicked. As a result, the request will be happening before we click the button, not when.

See? No more flickers.

Amongst others, here are some amazing features you get out of the box from SWR:

  • Reusable data fetching
  • Inbuilt cache
  • SSR/ISR/SSG support
  • TypeScript support
  • Mutations and revalidations

And a lot more performance benefits that we can’t cover within the scope of this post. As a next step, I’d like you to check out the SWR docs for more details on usage and benefits.

Complex use case

Let’s explore a more complex use case.

Mutation and revalidation with useSWR

So let’s say that we had our own /friends endpoint where we can retrieve all our friends from the database and also post to the endpoint to add new friends.

We can immediately write the code to look like this:

import useSWR from "swr";  
  const address = `http://localhost:3000/api/friends`;  
  const fetcher = (...args) => fetch(...args).then((res) => res.json());
  const { data, error } = useSWR(address, fetcher);
  const addFriend = async () => {
    const newUser = {
      name: "Robbie Doe",
      location: "Lagos",
      age: 20,
      email: "[email protected]",
      image:
        "https://www.shareicon.net/data/2016/09/15/829474_user_512x512.png",
    };
    await fetcher(address, {
      method: "POST",
      body: JSON.stringify(newUser),
    });
  };

When we click a button to trigger the addFriend() function, we’ll use useSWR to post to the /friends endpoint, effectively adding a new friend to the database. However, our site will not “know” that there’s been an update and therefore we won’t see that friend on screen.

This is where the useSWR mutation comes in. We can use mutation to revalidate our page to:

  1. Check if there’s new data
  2. If there is, revalidate the page and render the new data without triggering a full page reload:
import useSWR from "swr";  
  const address = `http://localhost:3000/api/friends`;  
  const fetcher = (...args) => fetch(...args).then((res) => res.json());
  const { data, error } = useSWR(address, fetcher);
  const addFriend = async () => {
    const newUser = {
      name: "Robbie Doe",
      location: "Lagos",
      age: 20,
      email: "[email protected]",
      image:
        "https://www.shareicon.net/data/2016/09/15/829474_user_512x512.png",
    };
    await fetcher(address, { 
      method: "POST",
      body: JSON.stringify(newUser),
    });
    mutate(address);
  };

Now, when we update the database by posting a new friend, useSWR will revalidate the page and ensure that we don’t serve stale data.

That said, useSWR will automatically revalidate your page on focus to ensure that your data is kept up to date. But that could be a lot. As a result, you have the option to opt out of revalidation on focus by passing this option into SWR as an argument:

  const { data, error } = useSWR(address, fetcher, {
    revalidateOnFocus: false
  });

Conclusion

In this post, we’ve gone through the basics of the useSWR hook. We also built a mini random user generator app with Next.js to demonstrate the SWR functionalities. I hope this post has provided you some insight into fetching data in Next.js applications with useSWR. The live demo of the site we built is hosted here on Netlify. Feel free to play around with it and also fork the codebase from GitHub if you want to tweak things to your taste.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Peter Ekene Eze Learn, Apply, Share

Leave a Reply