Rendering and data fetching in NextJS

Rendering strategies

  • Static Site Generation (SSG) uses the serverside-only method getStaticProps() to provide your React component with data that is then used to pre-render your components at build time. The pre-rendered HTML is then reused on each request. This is the default and recommended rendering method as it results in better performance, caching and SEO.
  • Server-side Rendering (SSR) uses the serverside-only method getServerSideProps() which pre-renders the HTML of your component on each request.
  • Client-side Rendering (CSR) uses the browser-only JS engine to render components
  • Incremental Static Regeneration (ISR) uses the serverside-only method getStaticProps() and allow you to create or update static pages after you’ve built your site. So you can create static pages without needing to rebuild the entire site.

Each generated HTML is associated with minimal JavaScript code necessary for that page. When a page is loaded by the browser, its JavaScript code runs and makes the page fully interactive. This process is called hydration.

How hydration works

Hydration is the process by which React “attaches” to existing HTML that was already rendered by React in a server environment. During hydration, React will attempt to attach event listeners to the existing markup and take over rendering the app on the client1.

Here are the steps involved in hydration:

  1. The server generates HTML for a page and sends it to the client.
  2. The client receives the HTML and displays it to the user.
  3. The client also receives a JavaScript bundle that contains the React code for the page.
  4. The JavaScript bundle is executed, and React starts up on the client.
  5. React reads the existing HTML on the page and creates a virtual DOM that represents it.
  6. React compares the virtual DOM to what it would have generated if it had rendered the page from scratch.
  7. If there are any differences between the two, React will update the DOM to match its expected state.
  8. React attaches event listeners to the DOM and takes over rendering the app on the client.

During this process, React will attempt to preserve as much of the existing HTML as possible while updating it to match its expected state. If there are any differences between the server-generated HTML and what React would have generated, React will patch up these differences during hydration.

Let me know if this helps or if you have any further questions.

Choosing the right rendering strategy

You can create a “hybrid” NextJS app by using Static Generation for most pages and using Server-side Rendering for others.

Static Site Generation (SSG) via getStaticProps

You should use getStaticProps if:

  • The data required to render the page is available at build time ahead of a user’s request.
  • The data comes from a headless CMS.
  • The data can be publicly cached (not user-specific).
  • The page must be pre-rendered (for SEO) and be very fast — getStaticProps generates HTML and JSON files, both of which can be cached by a CDN for performance.

If you export an async getStaticProps function, it will get called at build time and fetch external data before it is send as props to the page. For example your blog page might need to fetch the list of blog posts from a CMS.

export default function Home(props) { ... }

export async function getStaticProps(context) {
  // context = {
  //   params - route parameters for pages using dynamic routes
  //   preview - is true if the page is in the preview mode
  //   previewData - contains the preview data set by setPreviewData
  //   locale - contains the active locale (if enabled)
  //   locales - contains all supported locales (if enabled)
  //   defaultLocale - contains the configured default locale (if enabled)
  // }

  // You should not use fetch() to call an API route in getStaticProps.
  // Instead, directly import the logic used inside your API route

  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be passed to the `Home` component
  return {
    // A required object with the props that will be received by the page component
    props: ...  
    // An optional amount in seconds after which a page re-generation can occur (see Incremental Static Regeneration)
    revalidate: ... 
    // An optional boolean value to allow the page to return a 404 status and page
    notFound: ... 
    // An optional redirect value to allow redirecting to internal and external resources
    redirect: {
      destination: '/',
      permanent: false,
    }
  }
}
  • In development mode, getStaticProps runs on each request instead.
  • Next.js polyfills fetch() on both the client and server. You don’t need to import it.
  • getStaticProps only runs on the server-side, that means you can write code such as direct database queries without them being sent to browsers.
  • getStaticProps can only be exported from a page. You can’t export it from non-page files.
  • getStaticProps runs in the background when using fallback: truegetStaticProps is called before initial render when using fallback: blocking

When a page with getStaticProps is pre-rendered at build time, in addition to the page HTML file, Next.js generates a JSON file holding the result of running getStaticProps. This JSON file will be used in client-side routing through next/link or next/router. When you navigate to a page that’s pre-rendered using getStaticProps, Next.js fetches this JSON file (pre-computed at build time) and uses it as the props for the page component.

When using Incremental Static Generation, getStaticProps will be executed in the background to generate the JSON needed for client-side navigation. You may see this in the form of multiple requests being made for the same page, however, this is intended and has no impact on end-user performance.

Server Side Rendering (SSR) via getServerSideProps

Server Side Rendering is a good idea if you cannot pre-render a page ahead of a user’s request with Static Site Generation, because the requested data is request specific, such as authorization headers, geo location or if your page shows frequently updated data that changes with every request. In that case, you can use Server-side Rendering which will be slower than statically generated pages, but the pre-rendered page will always be up-to-date. Pages will only be cached if cache-control headers are configured. If you do not need to render the data during the request, then you should consider fetching data on the client side or getStaticProps.

To use Server-side Rendering, you need to export getServerSideProps instead of getStaticProps from your page. You can’t export it from non-page files.

getStaticProps can only be exported from a page.

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 } }
}

export default Page

Client-side Rendering

With this approach you first statically generate (pre-render) parts of the page that do not require external data. Then, when the page loads, you fetch external data client-side. This works well for user dashboard pages, because a dashboard is a private, user-specific page where SEO is not relevant. The data is frequently updated, which requires request-time data fetching.

You can use any client side library that supports SSR to fetch data, but the team behind NextJS has created a React hook for data fetching called SWR. It is highly recommended for REST APIs if you’re fetching data on the client side. It handles caching, revalidation, focus tracking, refetching on interval, and more.

import useSWR from 'swr'

function Profile() {
  const { data, error } = useSWR('/api/user', fetch)

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

Statically Generated Pages with Dynamic Routes via getStaticPaths

If you want to statically generate a page at a path called /posts/<id> where <id> can be dynamic, then you first have to create a page /pages/posts/[id].js (explicitly use brackets in the filename).

getStaticPaths must be used with getStaticProps and will by default only run during build in production, but getStaticPaths allows you to control which pages are generated during the build instead of on-demand with fallback. For example, you can defer generating all pages on-demand by returning an empty array for paths.

In development (next dev), getStaticPaths will be called on every request.

getStaticPath must return an object containing paths as key which itself holds an array with objects that must have params as key. The params strings are case-sensitive. For example, if the page name is pages/posts/[postId]/[commentId], then params should contain postId and commentId:

{
  paths: [
    {
      params: {
        postId: '1',
        commentId: '53'
      }
    },
    {
      params: {
        postId: '2',
        commentId: '98'
      }
    }
  ]
}

If the page name uses catch-all routes like pages/[...slug]:

{
  paths: [
    {
      params: {
        slug: ['hello', 'world'] // would generate the page for url /hello/world
      }
    }
  ]
}

If you provide an optional catch-all-route like pages/[[...slug]]:

{
  paths: [
    {
      params: {
        slug: false // would generate the page for the root url /
      }
    }
  ]
}
import Layout from '../../components/layout'

export default function Post() {
  return <Layout>
    <Head>
        <title>{postData.title}</title>
    </Head>
  </Layout>
}

export async function getStaticPaths() {
  // Return a list of possible value for id
  const paths = getAllPostIds();

  // paths MUST be an array of objects, each
  // containing a 'params' key and an object with 'id' because
  // we are using [id] in the file name.
  // Example { params: { id: 4711  }  }

  return {
    paths,
    // if fallback is false, any paths not returned by getStaticPaths will result in a 404 page
    // if fallback is true, getStaticProps runs in the background
    // if fallback is 'blocking', getStaticProps is called before initial render
    fallback: false
  }
}

export async function getStaticProps({ params }) {
  // Fetch necessary data for the blog post using params.id
  const postData = getPostData(params.id)

  return {
    props: {
      postData
    }
  }
}

Fallback for getStaticPaths

If fallback is false, then any paths not returned by getStaticPaths will result in a 404 page. This option is useful if you have a small number of paths to create, or new page data is not added often.

Fallback ‘true’

fallback: true is useful if your app has a very large number of static pages that depend on data (such as a very large e-commerce site). If you want to pre-render all product pages, the builds would take a very long time. Instead, you may statically generate a small subset of pages and use fallback: true for the rest. fallback: true will not update generated pages, for that take a look at Incremental Static Regeneration. The behavior of getStaticProps changes:

  • The paths returned from getStaticPaths will be rendered to HTML at build time by getStaticProps.
  • The paths that have not been generated at build time will not result in a 404 page. Instead, NextJS will serve a “fallback” version of the page on the first request to such a path (see “Fallback pages” below for details).
  • In the background, NextJS will statically generate the requested path HTML and JSON. This includes running getStaticProps.
  • When that’s done, the browser receives the JSON for the generated path. This will be used to automatically render the page with the required props. From the user’s perspective, the page will be swapped from the fallback page to the full page.
  • At the same time, NextJS adds this path to the list of pre-rendered pages. Subsequent requests to the same path will serve the generated page, just like other pages pre-rendered at build time.
  • fallback: true is not supported when using next export.
// pages/posts/[id].js
import { useRouter } from 'next/router'

function Post({ post }) {
  const router = useRouter()

  // If the page is not yet generated, this will be displayed
  // initially until getStaticProps() finishes running
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // Render post...
}

// This function gets called at build time
export async function getStaticPaths() {
  return {
    // Only `/posts/1` and `/posts/2` are generated at build time
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // Enable statically generating additional pages
    // For example: `/posts/3`
    fallback: true,
  }
}

// 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 },
    // Re-generate the post at most once per second
    // if a request comes in
    revalidate: 1,
  }
}

export default Post

Fallback ‘blocking’

If fallback is 'blocking', new paths not returned by getStaticPaths will wait for the HTML to be generated, identical to SSR (hence why blocking), and then be cached for future requests so it only happens once per path.

fallback: 'blocking' will not update generated pages by default. To update generated pages, use Incremental Static Regeneration in conjunction with fallback: 'blocking'

Fallback pages

The page’s props will be empty for fallback pages. Using the router, you can detect if the fallback is being rendered:

function Post({ post }) {
  const router = useRouter()

  // If the page is not yet generated, this will be displayed
  // initially until getStaticProps() finishes running
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // Render post...
}

Incremental Static Regeneration

Incremental Static Regeneration allows you to update existing pages by re-rendering them in the background as traffic comes in. This ensures traffic is served uninterruptedly, always from static storage, and the newly built page is pushed only after it’s done generating.

// 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 second
    revalidate: 1, // In seconds
  }
}

export default Blog

Reading files within getStaticProps

Since Next.js compiles your code into a separate directory you can’t use __dirname. Instead you can use process.cwd():

import { promises as fs } from 'fs'
import path from 'path'

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>
          <h3>{post.filename}</h3>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  )
}

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries. See the "Technical details" section.
export async function getStaticProps() {
  const postsDirectory = path.join(process.cwd(), 'posts')
  const filenames = await fs.readdir(postsDirectory)

  const posts = filenames.map(async (filename) => {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = await fs.readFile(filePath, 'utf8')

    // Generally you would parse/transform the contents
    // For example you can transform markdown to HTML here

    return {
      filename,
      content: fileContents,
    }
  })
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts: await Promise.all(posts),
    },
  }
}

export default Blog

Accessing dynamic route parameters on client side

getStaticPaths is the preferred way to access route parameters, because it allows you to build static pages at runtime. But if you need to access route parameters client side, you could use this:

// pages/post/[pid].js will match for example:
// /post/abc          -> { "pid": "abc" }
// /post/1            -> { "pid": 1 }
// /post/abc?foo=bar  -> { "foo": "bar", "pid": "abc" }
// /post/abc?pid=123  -> { "pid": "abc" } route param pid=abc has priority over query param pid=123

// page pages/post/[pid]/[comment].js -> { "pid": "abc", "comment": "a-comment" }

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  const { pid } = router.query

  return <p>Post: {pid}</p>
}

export default Post

About Author

Mathias Bothe To my job profile

I am Mathias from Heidelberg, Germany. I am a passionate IT freelancer with 15+ years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I create Bosycom and initiated several software projects.