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:
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:
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:
- Check if there’s new data
- 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.