Devs! Try out Prismic's new onboarding and get $50 on us! 🎁Apply Now
Tech stack
·10 min read

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:

Mean Girls scene gif of Regina George saying, "Stop trying to make fetch happen."

Mean Girls gif source

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:

  1. The blog post (with the actual text, author, etc.)
  2. 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:

  1. 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.
  2. 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() directly in our 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:

  1. The data changes frequently.
  2. The data is provided through a JSON API, so we can easily query it using fetch.
  3. 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.
  4. 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 or getServerSideProps 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.
Wireframe of a page showing, on the left, a list of cards and on the right a sidebar. One card is highlighted in orange. The sidebar is also orange and is intended to show a linformation about the selected card.

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 with getStaticPaths if there's a dynamic route).
  • revalidate: An easy add-on to getStaticProps 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 🚀
Article written by

Alexander Dubovoy

Originally from the San Francisco Bay Area, Alexander Dubovoy is a Berlin-based coder and musician. He graduated from Yale University in May 2016, where he wrote an award-winning thesis on the history of jazz in the Soviet Union. Since graduating, he has worked as a freelance web developer. He teaches at Le Wagon, an international coding bootcamp, and loves helping students with tricky technical problems. He also manages an active performance schedule as an improvising musician. He loves to combine his passions for technology and music, particularly through his work at Groupmuse, a cooperative concert-presenting organization.

More posts
Alexander Dubovoy

Join the discussion

Hit your website goals

Websites success stories from the Prismic Community

How Arcadia is Telling a Consistent Brand Story

Read Case Study

How Evri Cut their Time to Ship

Read Case Study

How Pallyy Grew Daily Visitors from 500 to 10,000

Read Case Study