Data fetching and rendering techniques in Next.js

Date

22 APRIL 2024

Author

Ivan Ćorković

Data fetching in Next.js allows you to render your content in different ways, depending on your application’s use case. These include pre-rendering with Server-side Rendering or Static Generation, and updating or creating content at runtime with Incremental Static Regeneration.

We covered connecting Next.js to a headless WordPress here, and the example shown there is great for showcasing these techniques. Next.js has special functions for data fetching that are used differently depending on the way you wish to render your content, all very well documented here.

Server-side Rendering (SSR)

If a page uses Server-side Rendering, the page HTML is generated on each request.

To use Server-side Rendering for a page, you need to export an async function called getServerSideProps. This function will be called by the server on every request.

For example, suppose that your page needs to pre-render frequently updated data (fetched from an external API). You can write getServerSideProps which fetches this data and passes it to Page like below:

1 2 3 4 5 6 7 8 9 10 11 12 13 export default function Page({ data }) { // Render data... } // This gets called on every request export async function getServerSideProps() { // Fetch data from external API const res = await fetch(`https://.../data`) const data = await res.json() // Pass data to the page via props return { props: { data } } }

You should use getServerSideProps if you need to render a page that relies on personalized user data, or information that can only be known at request time. For example, authorization headers or a geolocation.

If you do not need to fetch the data at request time, or would prefer to cache the data and pre-rendered HTML, you should be using getStaticProps.

Behaviour

  • getServerSideProps runs on the server.
  • getServerSideProps can only be exported from a page.
  • getServerSideProps returns JSON.
  • When a user visits a page, getServerSideProps will be used to fetch data at request time, and the data is used to render the initial HTML of the page.
  • props passed to the page component can be viewed on the client as part of the initial HTML. This is to allow the page to be hydrated correctly. Make sure that you don’t pass any sensitive information that shouldn’t be available on the client in props.
  • When a user visits the page through next/link or next/router, Next.js sends an API request to the server, which runs getServerSideProps.
  • You do not have to call a Next.js API Route to fetch data when using getServerSideProps since the function runs on the server. Instead, you can call a CMS, database, or other third-party APIs directly from inside getServerSideProps.

Static Site Generation (SSG)

If a page uses Static Generation, the page HTML is generated at build time. That means in production, the page HTML is generated when you run next build. This HTML will then be reused on each request. It can be cached by a CDN.

In Next.js, you can statically generate pages with or without data. Let’s take a look at each case.

Static Generation without data

By default, Next.js pre-renders pages using Static Generation without fetching data. Here’s an example:

1 2 3 4 5 function About() { return <div>About</div> } export default About

Note that this page does not need to fetch any external data to be pre-rendered. In cases like this, Next.js generates a single HTML file per page during build time.

Static Generation with data

Some pages require fetching external data for pre-rendering. There are two scenarios, and one or both might apply. In each case, you can use these functions that Next.js provides:

  1. Your page content depends on external data: Use getStaticProps.
  2. Your page paths depend on external data: Use getStaticPaths (usually in addition to getStaticProps).

Scenario 1: Your page content depends on external data

Example: Your blog page might need to fetch the list of blog posts from a CMS (content management system).

1 2 3 4 5 6 7 8 9 10 11 // TODO: Need to fetch `posts` (by calling some API endpoint) // before this page can be pre-rendered. export default function Blog({ posts }) { return ( <ul> {posts.map((post) => ( <li>{post.title}</li> ))} </ul> ) }

To fetch this data on pre-render, Next.js allows you to export an async function called getStaticProps from the same file. This function gets called at build time and lets you pass fetched data to the page’s props on pre-render.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export default function Blog({ posts }) { // Render posts... } // This function gets called at build time export async function getStaticProps() { // Call an external API endpoint to get posts const res = await fetch('https://.../posts') const posts = await res.json() // By returning { props: { posts } }, the Blog component // will receive `posts` as a prop at build time return { props: { posts, }, } }

To learn more about how getStaticProps works, check out the Next.js Data Fetching documentation.

Scenario 2: Your page paths depend on external data

Next.js allows you to create pages with dynamic routes. For example, you can create a file called pages/posts/[id].js to show a single blog post based on id. This will allow you to show a blog post with id: 1 when you access posts/1.

However, which id you want to pre-render at build time might depend on external data.

Example: suppose that you’ve only added one blog post (with id: 1) to the database. In this case, you’d only want to pre-render posts/1 at build time.

Later, you might add the second post with id: 2. Then you’d want to pre-render posts/2 as well.

So your page paths that are pre-rendered depend on external data. To handle this, Next.js lets you export an async function called getStaticPaths from a dynamic page (pages/posts/[id].js in this case). This function gets called at build time and lets you specify which paths you want to pre-render.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // This function gets called at build time export async function getStaticPaths() { // Call an external API endpoint to get posts const res = await fetch('https://.../posts') const posts = await res.json() // Get the paths we want to pre-render based on posts const paths = posts.map((post) => ({ params: { id: post.id }, })) // We'll pre-render only these paths at build time. // { fallback: false } means other routes should 404. return { paths, fallback: false } }

Also in pages/posts/[id].js, you need to export getStaticProps so that you can fetch the data about the post with this id and use it to pre-render the page:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export default function Post({ post }) { // Render post... } export async function getStaticPaths() { // ... } // This also gets called at build time export async function getStaticProps({ params }) { // params contains the post `id`. // If the route is like /posts/1, then params.id is 1 const res = await fetch(`https://.../posts/${params.id}`) const post = await res.json() // Pass post data to the page via props return { props: { post } } }

To learn more about how getStaticPaths works, check out the Data Fetching documentation.

When to use Static Generation?

It is recommended using Static Generation (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

You can use Static Generation for many types of pages, including:

  • Marketing pages
  • Blog posts and portfolios
  • E-commerce product listings
  • Help and documentation

You should ask yourself: “Can I pre-render this page ahead of a user’s request?” If the answer is yes, then you should choose Static Generation.

On the other hand, Static Generation is not a good idea if you cannot pre-render a page ahead of a user’s request. Maybe your page shows frequently updated data, and the page content changes on every request.

In cases like this, you can do one of the following:

  • Use Static Generation with Client-side data fetching: You can skip pre-rendering some parts of a page and then use client-side JavaScript to populate them. To learn more about this approach, check out the Data Fetching documentation.
  • Use Server-Side Rendering: Next.js pre-renders a page on each request. It will be slower because the page cannot be cached by a CDN, but the pre-rendered page will always be up-to-date.

Incremental Static Regeneration (ISR)

Next.js allows you to create or update static pages after you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, without needing to rebuild the entire site. With ISR, you can retain the benefits of static while scaling to millions of pages.

To use ISR, add the revalidate prop to getStaticProps:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 function Blog({ posts }) { return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) } // This function gets called at build time on server-side. // It may be called again, on a serverless function, if // revalidation is enabled and a new request comes in export async function getStaticProps() { const res = await fetch('https://.../posts') const posts = await res.json() return { props: { posts, }, // Next.js will attempt to re-generate the page: // - When a request comes in // - At most once every 10 seconds revalidate: 10, // In seconds } } // This function gets called at build time on server-side. // It may be called again, on a serverless function, if // the path has not been generated. export async function getStaticPaths() { const res = await fetch('https://.../posts') const posts = await res.json() // Get the paths we want to pre-render based on posts const paths = posts.map((post) => ({ params: { id: post.id }, })) // We'll pre-render only these paths at build time. // { fallback: 'blocking' } will server-render pages // on-demand if the path doesn't exist. return { paths, fallback: 'blocking' } } export default Blog

When a request is made to a page that was pre-rendered at build time, it will initially show the cached page.

  • Any requests to the page after the initial request and before 10 seconds are also cached and instantaneous.
  • After the 10-second window, the next request will still show the cached (stale) page
  • Next.js triggers a regeneration of the page in the background.
  • Once the page generates successfully, Next.js will invalidate the cache and show the updated page. If the background regeneration fails, the old page would still be unaltered.

When a request is made to a path that hasn’t been generated, Next.js will server-render the page on the first request. Future requests will serve the static file from the cache.

On-Demand Revalidation

If you set a revalidate time of 60, all visitors will see the same generated version of your site for one minute. The only way to invalidate the cache is from someone visiting that page after the minute has passed.

Starting with v12.2.0, Next.js supports On-Demand Incremental Static Regeneration to manually purge the Next.js cache for a specific page. This makes it easier to update your site when:

  • Content from your headless CMS is created or updated
  • Ecommerce metadata changes (price, description, category, reviews, etc.)

Inside getStaticProps, you do not need to specify revalidate to use on-demand revalidation. If revalidate is omitted, Next.js will use the default value of false (no revalidation) and only revalidate the page on-demand when revalidate() is called.

Using On-Demand Revalidation

First, create a secret token only known by your Next.js app. This secret will be used to prevent unauthorized access to the revalidation API Route. You can access the route (either manually or with a webhook) with the following URL structure:

https://<your-site.com>/api/revalidate?secret=<token>

Next, add the secret as an Environment Variable to your application. Finally, create the revalidation API Route:

export default async function handler(req, res) { // Check for secret to confirm this is a valid request if (req.query.secret !== process.env.MY_SECRET_TOKEN) { return res.status(401).json({ message: 'Invalid token' }) } try { // this should be the actual path not a rewritten path // e.g. for "/blog/[slug]" this should be "/blog/post-1" await res.revalidate('/path-to-revalidate') return res.json({ revalidated: true }) } catch (err) { // If there was an error, Next.js will continue // to show the last successfully generated page return res.status(500).send('Error revalidating') } }

It is important to note, when running locally with next dev, getStaticProps is invoked on every request. To verify your on-demand ISR configuration is correct, you will need to create a production build and start the production server:

1 2 $ next build $ next start

Then, you can confirm that static pages have successfully revalidated.