Making Fetch Happen? When to Use Different Data-Fetching Methods in Next.js
One of the reasons why Next.js has become such a popular framework for building React-based websites is that it provides so many different ways to load data from outside sources.
In a typical React app (without Next.js), our only option would be to use fetch
(or a similar alternative) to trigger HTTP requests. In other words, all the data-fetching would happen on the client-side
. Some data, however, doesn't change very frequently and should be cached, or sometimes we don't want the user to see and replicate the request we're making (perhaps for security reasons).
Next.js provides us with techniques to fetch data on the server-side
. Though the Next.js data fetching documentation gives a comprehensive overview of the different options, it can take a little know-how to figure out which approach to take for which circumstance. Consequently, I want to share not just how Next.js handles data-fetching but why we should use a particular approach. Otherwise, we can get confused and end up feeling a bit like this:
Rather than discuss in the abstract, we're going to build a project together to simulate various real-world situations. Specifically, we'll build a blog with a comments feature. In order to do so, we'll need two pieces of information:
- The blog post (with the actual text, author, etc.)
- The list of comments
These two parts differ in a fundamental way. The blog post is unlikely to change frequently. It assumedly will only update when a site admin edits the text on the Content Management System (CMS). The comments, on the other handle, will change frequently. We're going to assume our blog is really popular, so people are writing new comments every minute. 🙂
getStaticProps
As we discussed above, the data for our blog post probably won't change very often and will come from a CMS. We'll use Prismic as our CMS (this is the Prismic blog we're reading after all!), but this example isn't limited to Prismic exclusively. In order to follow along, I'd recommend you first check out the Prismic tutorial on setting Prismic up with Next.js.
For today, we're only going to build the page for each individual blog post, not an index page for all of them. Consequently, we'd need our URLs to look something like /making-fetch-happen-with-nextjs
. We need a unique slug in the URL to identify which blog post we're looking at. So, let's add a new file /pages/[uid].js
. The [ ]
square brackets indicate that the URL can change (a "dynamic route") and that the value of the changing parameter will be stored under params.uid
.
Which data-fetching approach would be best to use here? Our data changes infrequently, so it doesn't make sense to reload the data every time a user loads the page. As a result, we can rule out client-side approaches entirely.
Instead, the ideal option is to load the data one time on the server-side when we build our site. Since the data is going to be the same for every user of our site, it's far more efficient to load it one time on the server than to make every client load it individually. Then, for added efficiency, we want to cache the result and only serve that rendered HTML to the client.
In Next.js, the getStaticProps
function implements this behavior exactly. We have to export a function by this name in a page component. So, we can add something like the following to our /pages/[uid].js
file:
// `Client` should be an initiialized Prismic client
// We haven't written this config file, and you wouldn't have to use this path specifically, but it should look something similar to here: https://prismic.io/docs/technologies/query-api-nextjs
import { Client } from '/config/prismic_client_config'
export async function getStaticProps({ params }) {
const postUid = params.uid
const post = await Client().getByUID('page', postUid)
return {
props: {
post,
},
}
}
Now, when we build our site, we load the information for our post
from Prismic. The
return
statement is formatted so that this page will now receive a
prop
called
post
. We'll just deal with the data here, but we would want to use the props.post in our page component to display the data (a full example is available in the Prismic docs).
We're going to run into one issue though: remember that our page's URL can change dynamically (it has square brackets in the filename). So, we need to render a different page for every blog post. Right now, Next.js only knows how to find the data for one blog post, but it doesn't know the list of all the different blog posts that it needs to pre-render. So, whenever we have a dynamic route and getStaticProps
, we also have to use the closely-related
getStaticPaths
function to tell Next.js which URLs to render:
export async function getStaticPaths() {
const docs = await Client().query(
// Prismic predicates tell the API which documents to retrieve from the repository.
Prismic.Predicates.at('document.type', 'page'),
{ lang: '*' }
)
return {
paths: docs.results.map((doc) => {
return { params: { uid: doc.uid } }
}),
fallback: false,
}
}
Here, we'll load the list of all the posts from Prismic, and then we'll return an array of all the uid
routes we want to pre-render. Finally, the fallback
value of false
means we send the user to a 404 error page if they try to go to a uid
that's not in the list (you can explore the other options in the Next.js docs).
Stay on Top of New Tools, Frameworks, and More
Research shows that we learn better by doing. Dive into a monthly tutorial with the Optimized Dev Newsletter that helps you decide which new web dev tools are worth adding to your stack.
getServerSideProps
We have our blog posts loading, so now it's time to build our comments feature. To simulate that we're using a comment service and that our blog is super popular, I've built a little API at https://random-comment-generator.herokuapp.com/ that returns a random list of 20 different comments at every request.
The first thing we might be tempted to do is load our comments inside the getStaticProps
function from the previous example. But, remember that getStaticProps
is only being run once at build-time. Our comments are flooding in every second. If we use getStaticProps
, they'll never update. 🙀
The other Next.js function we could reach for is called getServerSideProps
, but I wouldn't recommend it in this case. It would be a pretty easy refactor because the APIs are basically identical; we'd just change the word
getStaticProps
to
getServerSideProps
and we'd have to remove the
getStaticPaths
function entirely.
The difference is that getServerSideProps
runs every time a user loads the page, so now our comments will update in real time. But, it is also inefficient. The user has to wait and won't see anything until both the blog post and comments are loaded by the server. And, we lose our magical ability to cache the blog post, so it's loaded repeatedly with every page view.
Indeed, getServerSideProps
is the data-fetching method I probably rely on the least. It's not well-suited to building something like a blog where caching is important. It's also not ideal for our comments, which come from an API, because they don't have to be loaded on the server-side for any reason. When is it appropriate to use getServerSideProps
? I've highlighted two main situations:
- When we want to fetch data that relates to the user's cookies/activity and is consequently not possible to cache. Websites with user authentication features end up relying on
getServerSideProps
a lot to make sure users are properly signed in before fulfilling a request. - When we are fetching frequently-updated data directly from a database. One of the unique features of Next.js is that, because it allows us to fetch data on the server-side, we don't always need to build an API for our apps to have a back-end. Instead, we can actually make requests directly to a database using an Object Relational Model (ORM) like Prisma, so we could imagine something like
const users = await prisma.user.findMany()
getServerSideProps
.
getServerSideProps
has specific use cases, but it generally isn't suited to building a blog.
Note: I should additionally note that, if we're using getServerSideProps
, we need to deploy using a service that can run our data-fetching functions. Services like Vercel or Netlify certainly can, so it's usually fine. If, however, we only use getStaticProps
, then we could deploy our static site even more simply through something like a CDN because we're not using any server-side code at run-time.
The revalidate parameter and incremental static regeneration
In order to load our comments, we need to seek a better data-fetching solution. One approach we may be wondering about is if there's any way to keep the caching from getStaticProps
but just have it update every few minutes. Our blog is popular enough that a caching approach isn't totally ideal, since our comments won't be up-to-the-minute accurate (we'll see how to do this soon), but it would be a big improvement. Fortunately, it turns out there's an easy way to do so in Next.js.
Let's add two things. The first is a fetch
request to get the comments from our slightly silly API. The second is a revalidate
parameter of one minute (or 60,000 milliseconds). What does this do?
export async function getStaticProps({params}) {
const postUid = params.uid
const post = (await Client().getByUID('page', postUid))
// ADDED
const commentsResponse = await fetch("https://random-comment-generator.herokuapp.com/")
const { comments } = await commentsResponse.json()
return {
props: {
post,
comments,
},
// ADDED
revalidate: 60_000
}
}
Like before, our getStaticProps
will keep our page cached. So, the user always receives a cached version (and the associated performance boost).
Now, our comments
will be the props.comments
on our page component. So, we could build a simple components/CommentsList.jsx
file to display them:
const CommentsList = (props) => {
const { comments } = props
return (
<ul role="list">
{comments.map((comment) => (
<li key={comment.token}>{comment.content}</li>
))}
</ul>
)
}
export default CommentsList
And then in the page, we could import that file and add:
<CommentsList comments={props.comments} />
But, now, if someone loads the page more than one minute after the previous cache, Next.js will re-render the page behind-the-scenes so that the next user will see a more recently updated version. Consequently, revalidate
allows us to ensure that this page is always reasonably up-to-date without constantly triggering builds of our whole site, a technique called Incremental Static Regeneration.
In my opinion, revalidate
is well-suited to this situation and extremely useful in general. It leverages Static Site Generation (SSG) and caching, without letting our data go stale. It's also quite easy to just add revalidate
onto any getStaticProps
function we want to keep incrementally updated.
Client-side fetching
You may have noticed that the choice between getStaticProps
and getServerSideProps
is kind of all-or-nothing; you can't use both at once. But what if we want to statically cache the blog post while dynamically loading the comments? Then, the answer is to use getStaticProps
only for the blog post and to use client-side fetching for the comments.
In my opinion, client-side fetching is most appropriate for the comments for these reasons:
- The data changes frequently.
- The data is provided through a JSON API, so we can easily query it using
fetch
. - We don't care if the user can see the HTTP request in the "Network" tab of their browser's inspector. In other words, there are no security concerns in exposing this request to the client.
- We're not being charged for every API request, so we don't have a particular interest in limiting the number of requests.
Though we could simply use fetch
to load this data, we're going to use SWR because it's also maintained by Vercel and commonly used in Next.js apps. We'll need to install it first:
yarn add swr
Unlike the server-side getStaticProps
and getServerSideProps
functions that can only be used in a page component, we can do client-side fetching inside of any component. So let's try rewriting our /components/CommentsList.jsx
file:
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json())
const COMMENTS_URL = 'https://random-comment-generator.herokuapp.com/'
const CommentsList = (props) => {
const { data, error } = useSWR(COMMENTS_URL, fetcher)
// because of the api, data will come in like: { comments: [{ content: ""}, ...] }
if (error) return <div>Something went wrong...</div>
if (!data) return <div>Loading...</div>
return (
<ul role="list">
{data.comments &&
data.comments.map((comment) => (
<li key={comment.token}>{comment.content}</li>
))}
</ul>
)
}
export default CommentsList
One of the advantages with SWR is that it helps us show error or loading messages while the data is fetching. SWR also handles caching for us, so if we make a request to the same URL in a different place, it will use the cached result instead of repeating the request. It's truly wild how often this pattern can replace more complicated state-management libraries.
There's no reason why we have to load all our data on the server-side. In this case, it probably makes the most sense to load and cache the blog post on the server-side but to load the comments on the client-side. Of course, there is an added layer of complexity in mixing multiple techniques, but the benefits generally outweigh this cost.
Server components
One of the benefits of client-side fetching is that the data-fetching can be scoped to individual components. We can take all the logic related to comments and put it inside our CommentsList
component. If we do server-side fetching, then all the logic has to go inside of the page component, which can lead to some awkward patterns:
- If we have to load lots of different types of data, our
getStaticProps
orgetServerSideProps
functions can get really bloated, and there are limited options for refactoring. - We may have to pass data down through tons of different unrelated components if we want to get it from the top-level page component to the relevant child component ("prop drilling").
- We may have data that needs to show up only on a part of a page and that changes based on user interaction, like in the diagram below. If we want to load this data on the server, then we have to load all the potential variants at the start (i.e. the data for every single card and sidebar combo), which means we would end up loading more data than the user is ever realistically going to view.
We could mitigate all these problems if we could do server-side fetching at the component level. The great news is that this is coming to React and Next.js quite soon with Server Components, and the alpha version is already available in Next.js 12.0. The bad news is that some of the implementation details and API will almost certainly change over the coming months, so I wouldn't advise building a site based on it or getting too hung up on the syntax quite yet.
Regardless, let's take a quick peek at how the alpha version works, so we can anticipate what's to come when these features are stable. I wouldn't recommend coding along with this example exactly (it requires updating to a beta version of React and a few other gymnastics at the moment), but rather I'd consider it more a "sneak preview." To enable Server Components, first we have to add the following to our next.config.js according to Vercel's blog post announcing Next.js 12:
module.exports = {
experimental: {
concurrentFeatures: true,
serverComponents: true
}
}
We then can have two different types of components in our app. For example, the CommentsList
component we were building might become components/CommentsList.client.jsx
. The client
means that this component is a "normal" React component and should come with client-side Javascript. If, however, we name the file components/CommentsList.server.jsx
, then we will render the HTML for the component on the server and deliver it without any Javascript. This means faster load times, but it also means that we can't use a lot of traditional React features like useState
because the component will essentially be raw HTML that's rendered through code executed on the server. Then, we could code our component like so:
const COMMENTS_URL = 'https://random-comment-generator.herokuapp.com/'
let cache
async function fetchData() {
const response = await fetch(COMMENTS_URL)
return response.json()
}
function useData() {
if (!cache) {
let data
let promise
cache = () => {
if (data !== undefined) return data
if (!promise) promise = fetchData().then((r) => (data = r))
throw promise
}
}
return cache()
}
const CommentsList = (props) => {
const data = useData()
return (
<ul role="list">
{data.comments &&
data.comments.map((comment) => (
<li key={comment.token}>{comment.content}</li>
))}
</ul>
)
}
export default CommentsList
And then, in our page component, we can do the exact same thing to render the page on the server side with a new /pages/[uid].server.js
. We can also use React's new Suspense API to fairly easily render a fallback.
import { Suspense } from 'react'
import CommentsList from '/components/CommentsList.server.jsx'
const PostPage = (props) => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<CommentsList />
</Suspense>
</div>
)
}
export default PostPage
You can check out Vercel's demo if you want a more full-fledged feature comparison. It's not difficult to imagine that, as the framework grows, the syntax will change. I'm also hoping that we'll end up with more sophisticated approaches to caching server components, similar to what we saw with getStaticProps
and revalidate
.
Server Components hold a lot of promise. Over time, we might be able to do away with special functions like getStaticProps
and getServerSideProps
and unify it all in a more modern API that operates on any component, rather than only on the page level. The future of data-caching is bright!
When to use each data-fetching method
As we've seen, there are a variety of different techniques for data caching in Next.js, and it can be a bit tricky to know which one to use when. We need to know whether data will update infrequently or frequently, as well as where it's coming from. As a TL;DR, here are my recommendations:
getStaticProps
: Any data that changes infrequently, particularly from a CMS. (Must be used withgetStaticPaths
if there's a dynamic route).revalidate
: An easy add-on togetStaticProps
if the data might change, and we're OK serving a cached version.getServerSideProps
: Primarily useful with data that must be fetched on the server that changes frequently or depends on user authentication.- Client-side with
use-swr
: Great for any data coming from an API that changes frequently. - Server Components: To use in the future 🚀