Skip to main content

Sitecore

A crash course of Next.js: rendering strategies and data fetching (part 1)

Next Course

This series is my Next.js study resume, and despite it’s keen to a vanilla Next.js,  all the features are applicable with Sitecore SDK. It is similar to the guide I recently wrote about GraphQL and aims reducing the learning curve for those switching to it from other tech stack.

Next.js is a React-based framework designed for developing web applications with functionality beyond SPA. As you know, the main disadvantage of SPA is problems with indexing pages of such applications by search robots, which negatively affects SEO. Recently the situation has begun to change for the better, at least the pages of my small SPA-PWA application started being indexed as normal. However, Next.js significantly simplifies the process of developing multi-page and hybrid applications. It also provides many other exciting features I am going to share with you over this course.

Pages

In Next.js a page is a React component that is exported from a file with a .js, .jsx, .ts, or .tsx extension located in the pages directory. Each page is associated with a route by its name. For example, the page pages/about.js will be accessible at /about. Please note that the page should be exported by default using export default:

export default function About() {
  return <div>About us</div>
}

The route for pages/posts/[id].js will be dynamic, i.e. such a page will be available at posts/1, posts/2, etc.

By default, all pages are pre-rendered. This results in better performance and SEO. Each page is associated with a minimum amount of JS. When the page loads, JS code runs, which makes it interactive (this process is called hydration).

There are 2 forms of pre-rendering: static generation (SSG, which is the recommended approach) and server-side rendering (SSR, which is more familiar for those working with other serverside frameworks such as ASP.NET). The first form involves generating HTML at build time and reusing it on every request. The second is markup generation on a server for each request. Generating static markup is the recommended approach for performance reasons.

In addition, you can use client-side rendering, where certain parts of the page are rendered by client-side JS.

SSG

It can generate both pages with data and without.

Without data case is pretty obvious:

export default function About() {
  return <div>About us</div>
}

There are 2 potential scenarios to generate a static page with data:

  1. Page content depends on external data: getStaticProps is used
  2. Page paths depend on external data: getStaticPaths is used (usually in conjunction with getStaticProps)

1. Page content depends on external data

Let’s assume that the blog page receives a list of posts from an external source, like a CMS:

// TODO: requesting `posts`

export default function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

To obtain the data needed for pre-rendering, the asynchronous getStaticProps function must be exported from the file. This function is called during build time and allows you to pass the received data to the page in the form of props:

export default function Blog({ posts }) {
  // ...
}

export async function getStaticProps() {
  const posts = await (await fetch('https://site.com/posts'))?.json()

  // the below syntax is important
  return {
    props: {
      posts
    }
  }
}

2. Page paths depend on external data

To handle the pre-rendering of a static page whose paths depend on external data, an asynchronous getStaticPaths function must be exported from a dynamic page (for example, pages/posts/[id].js). This function is called at build time and allows you to define prerendering paths:

export default function Post({ post }) {
  // ...
}

export async function getStaticPaths() {
  const posts = await (await fetch('https://site.com/posts'))?.json()

  // pay attention to the structure of the returned array
  const paths = posts.map((post) => ({
    params: { id: post.id }
  }))

  // `fallback: false` means that 404 uses alternative route
  return {
    paths,
    fallback: false
  }
}

pages/posts/[id].js should also export the getStaticProps function to retrieve the post data with the specified id:

export default function Post({ post }) {
  // ...
}

export async function getStaticPaths() {
  // ...
}

export async function getStaticProps({ params }) {
  const post = await (await fetch(`https://site.com/posts/${params.id}`)).json()

  return {
    props: {
      post
    }
  }
}

SSR

To handle server-side page rendering, the asynchronous getServerSideProps function must be exported from the file. This function will be called on every page request.

function Page({ data }) {
  // ...
}

export async function getServerSideProps() {
  const data = await (await fetch('https://site.com/data'))?.json()

  return {
    props: {
      data
    }
  }
}

Data Fetching

There are 3 functions to obtain the data needed for pre-rendering:

  • getStaticProps (SSG): Retrieving data at build time
  • getStaticPaths (SSG): Define dynamic routes to pre-render pages based on data
  • getServerSideProps (SSR): Get data on every request

getStaticProps

The page on which the exported asynchronous getStaticProps is pre-rendered using the props returned by this function.

export async function getStaticProps(context) {
  return {
    props: {}
  }
}

context is an object with the following properties:

    • params – route parameters for pages with dynamic routing. For example, if the page title is [id].js, the params will be { id: … }
    • preview – true if the page is in preview mode
    • previewData – data set using setPreviewData
    • locale – current locale (if enabled)
    • locales – supported locales (if enabled)
    • defaultLocale – default locale (if enabled)

getStaticProps returns an object with the following properties:

  • props – optional object with props for the page
  • revalidate – an optional number of seconds after which the page is regenerated. The default value is false – regeneration is performed only with the next build
  • notFound is an optional boolean value that allows you to return a 404 status and the corresponding page, for example:
export async function getStaticProps(context) {
  const res = await fetch('/data')
  const data = await res.json()

  if (!data) {
    return {
      notFound: true
    }
  }

  return {
    props: {
      data
    }
  }
}

Note that notFound is not required in fallback: false mode, since in this mode only the paths returned by getStaticPaths are pre-rendered.

Also, note that notFound: true means a 404 is returned even if the previous page was successfully generated. This is designed to support cases where user-generated content is removed.

  • redirect is an optional object that allows you to perform redirections to internal and external resources, which must have the form {destination: string, permanent: boolean}:
export async function getStaticProps(context) {
  const res = await fetch('/data')
  const data = await res.json()

  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false
      }
    }
  }

  return {
    props: {
      data
    }
  }
}

Note 1: Build-time redirects are not currently allowed. Such redirects must be declared at next.config.js.

Note 2: Modules imported at the top level for use within  getStaticProps are not included in the client assembly. This means that server code, including reads from the file system or from the database, can be written directly in getStaticProps.

Note 3: fetch() in getStaticProps should only be used when fetching resources from external sources.

Use Cases

  • rendering data is available at build time and does not depend on the user request
  • data comes from a headless CMS
  • data can be cached in plain text (and not user-specific data)
  • the page must be pre-rendered (for SEO purposes) and must be very fast – getStaticProps generates HTML and JSON files that can be cached using a CDN

Use it with TypeScript:

import { GetStaticProps } from 'next'

export const getStaticProps: GetStaticProps = async (context) => {}

To get the desired types for props you should use InferGetStaticPropsType<typeof getStaticProps>:

import { InferGetStaticPropsType } from 'next'

type Post = {
  author: string
  content: string
}

export const getStaticProps = async () => {
  const res = await fetch('/posts')
  const posts: Post[] = await res.json()

  return {
    props: {
      posts
    }
  }
}

export default function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
  // posts will be strongly typed as `Post[]`
}

ISR: Incremental static regeneration

Static pages can be updated after the application is built. Incremental static regeneration allows you to use static generation at the individual page level without having to rebuild the entire project.

Example:

const Blog = ({ posts }) => (
  <ul>
    {posts.map((post) => (
      <li>{post.title}</li>
    ))}
  </ul>
)

// Executes while building on a server.
// It can be called repeatedly as a serverless function when invalidation is enabled and a new request arrives
export async function getStaticProps() {
  const res = await fetch('/posts')
  const posts = await res.json()

  return {
    props: {
      posts
    },
    // `Next.js` will try regenerating a page:
    // - when a new request arrives
    // - at least once every 10 seconds
    revalidate: 10 // in seconds
  }
}

// Executes while building on a server.
// It can be called repeatedly as a serverless function if the path has not been previously generated
export async function getStaticPaths() {
  const res = await fetch('/posts')
  const posts = await res.json()

  // Retrieving paths for posts pre-rendering
  const paths = posts.map((post) => ({
    params: { id: post.id }
  }))

  // Only these paths will be pre-rendered at build time
  // `{ fallback: 'blocking' }` will render pages serverside in the absence of a corresponding path
  return { paths, fallback: 'blocking' }
}

export default Blog

When requesting a page that was pre-rendered at build time, the cached page is displayed.

  • The response to any request to such a page before 10 seconds have elapsed is also instantly returned from the cache
  • After 10 seconds, the next request also receives a cached version of the page in response
  • After this, page regeneration starts in the background
  • After successful regeneration, the cache is invalidated and a new page is displayed. If regeneration fails, the old page remains unchanged

Technical nuances

getStaticProps

  • Since getStaticProps runs at build time, it cannot use data from the request, such as query params or HTTP headers.
  • getStaticProps only runs on the server, so it cannot be used to access internal routes
  • when using getStaticProps, not only HTML is generated, but also a JSON file. This file contains the results of getStaticProps and is used by the client-side routing mechanism to pass props to components
  • getStaticProps can only be used in a page component. This is because all the data needed to render the page must be available
  • in development mode getStaticProps is called on every request
  • preview mode is used to render the page on every request

 

getStaticPaths

Dynamically routed pages from which the asynchronously exported getStaticPaths function will be pre-generated for all paths returned by that function.

export async function getStaticPaths() {
  return {
    paths: [
      params: {}
    ],
    fallback: true | false | 'blocking'
  }
}
Paths

paths defines which paths will be pre-rendered. For example, if we have a page with dynamic routing called pages/posts/[id].js, and the getStaticPaths exported on that page returns paths as below:

return {
  paths: [
    { params: { id: '1' } },
    { params: { id: '2' } },
  ]
}

Then the posts/1 and posts/2 pages will be statically generated based on the pages/posts/[id].js component.

Please note that the name of each params must match the parameters used on the page:

  • if the page title is pages/posts/[postId]/[commentId] then params should contain postId and commentId
  • if the page uses a route interceptor, for example, pages/[...slug], params must contain slug as an array. For example, if such an array looks as ['foo', 'bar'], then the page /foo/bar will be generated
  • If the page uses an optional route interceptor, using null, [], undefined, or false will cause the top-level route to be rendered. For example, applying slug: false to pages/[[...slug]], will generate the page /
Fallback
  • if fallback is false, the missing path will be resolved by a 404 page
  • if fallback is true, the behavior of getStaticProps will be:
      • paths from getStaticPaths will be generated at build time using getStaticProps
      • a missing path will not be resolved by a 404 page. Instead, a fallback page will be returned in response to the request
      • The requested HTML and JSON are generated in the background. This includes calling getStaticProps
      • the browser receives JSON for the generated path. This JSON is used to automatically render the page with the required props. From the user’s perspective, this looks like switching between the backup and full pages
    • the new path is added to the list of pre-rendered pages

Please note: fallback: true is not supported when using next export.

Fallback pages

In the fallback version of the page:

  • prop pages will be empty
  • You can determine that a fallback page is being rendered using the router: router.isFallback will be true
// pages/posts/[id].js
import { useRouter } from 'next/router'

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

  // This will be displayed if the page has not yet been generated, 
  // Until `getStaticProps` finishes its work
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // post rendering
}

export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } }
    ],
    fallback: true
  }
}

export async function getStaticProps({ params }) {
  const res = await fetch(`/posts/${params.id}`)
  const post = await res.json()

  return {
    props: {
      post
    },
    revalidate: 1
  }
}

export default Post

In what cases might fallback: true be useful? It can be useful with a truly large number of static pages that depend on data (for example, a very large e-commerce storefront). We want to pre-render all the pages, but we know the build will take forever.

Instead, we generate a small set of static pages and use fallback: true for the rest. When requesting a missing page, the user will see a loading indicator for a while (while getStaticProps doing its job), then see the page itself. After that, a new page will be returned in response to each request.

Please note: fallback: true does not refresh the generated pages. Incremental static regeneration is used for this purpose instead.

If fallback is set to blocking, the missing path will also not be resolved by the 404 page, but there will be no transition between the fallback and normal pages. Instead, the requested page will be generated on the server and sent to the browser, and the user, after waiting for some time, will immediately see the finished page

Use cases for getStaticPaths

getStaticPaths is used to pre-render pages with dynamic routing. Use it with TypeScript:

import { GetStaticPaths } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {}

Technical nuances:

  • getStaticPaths must be used in conjunction with getStaticProps. It cannot be used in conjunction with getServerSideProps
  • getStaticPaths only runs on the server at build time
  • getStaticPaths can only be exported in a page component
  • in development mode getStaticPaths runs on every request

getServerSideProps

The page from which the asynchronous getServerSideProps function is exported will be rendered on every request using the props returned by this function.

export async function getServerSideProps(context) {
  return {
    props: {}
  }
}

context is an object with the following properties:

  • params: see getStaticProps
  • req: HTTP IncomingMessage object (incoming message, request)
  • res: HTTP response object
  • query: object representation of the query string
  • preview: see getStaticProps
  • previewData: see getStaticProps
  • resolveUrl: a normalized version of the requested URL, with the _next/data prefix removed and the original query string values included
  • locale: see getStaticProps
  • locales: see getStaticProps
  • defaultLocale: see getStaticProps

getServerSideProps should return an object with the following fields:

  • props – see getStaticProps
  • notFound – see getStaticProps
export async function getServerSideProps(context) {
  const res = await fetch('/data')
  const data = await res.json()

  if (!data) {
    return {
      notFound: true
    }
  }

  return {
    props: {}
  }
}
  • redirect — see getStaticProps
export async function getServerSideProps(context) {
  const res = await fetch('/data')
  const data = await res.json()

  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false
      }
    }
  }

  return {
    props: {}
  }
}

For getServerSideProps there are the same features and limitations as getStaticProps.

Use cases for getServerSideProps

getServerSideProps should only be used when you need to pre-render the page based on request-specific data. Use it with TypeScript:

import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => {}

To get the expected types for props you should use InferGetServerSidePropsType<typeof getServerSideProps>:

import { InferGetServerSidePropsType } from 'next'

type Data = {}

export async function getServerSideProps() {
  const res = await fetch('/data')
  const data = await res.json()

  return {
    props: {
      data
    }
  }
}

function Page({ data }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  // ...
}

export default Page

Technical nuances:

  • getServerSideProps runs only serverside
  • getServerSideProps can only be exported in a page component

Client-side data fetching

If a page has frequently updated data, but at the same time this page doesn’t need to be pre-rendered (for SEO reasons), then it is pretty much possible to fetch its data directly at client-side.

The Next.js team recommends using their useSWR hook for this purpose, which provides features such as data caching, cache invalidation, focus tracking, periodic retrying, etc.

import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((res) => res.json())

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

  if (error) return <div>Error while retrieving the data</div>
  if (!data) return <div>Loading...</div>

  return <div>Hello, {data.name}!</div>
}

However, you’re not limited to it, old good React query fetch() functions also perfectly work for this purpose.

This concludes part 1. In part 2 we’ll talk about UI-related things coming OOB with Next.js – layouts, styles and fonts powerful features, Image and Script components, and of course – TypeScript.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Martin Miles

Martin is a Sitecore Expert and .NET technical solution architect involved in producing enterprise web and mobile applications, with 20 years of overall commercial development experience. Since 2010 working exclusively with Sitecore as a digital platform. With excellent knowledge of XP, XC, and SaaS / Cloud offerings from Sitecore, he participated in more than 20 successful implementations, producing user-friendly and maintainable systems for clients. Martin is a prolific member of the Sitecore community. He is the author and creator of the Sitecore Link project and one of the best tools for automating Sitecore development and maintenance - Sifon. He is also the founder of the Sitecore Discussion Club and, co-organizer of the Los Angeles Sitecore user group, creator of the Sitecore Telegram channel that has brought the best insight from the Sitecore world since late 2017.

More from this Author

Categories
Follow Us
TwitterLinkedinFacebookYoutubeInstagram