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:
- Page content depends on external data:
getStaticProps
is used - 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 timegetStaticPaths
(SSG): Define dynamic routes to pre-render pages based on datagetServerSideProps
(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 modepreviewData
– data set usingsetPreviewData
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 pagerevalidate
– an optional number of seconds after which the page is regenerated. The default value isfalse
– regeneration is performed only with the next buildnotFound
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 containpostId
andcommentId
- if the page uses a route interceptor, for example,
pages/[...slug]
,params
must containslug
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
, orfalse
will cause the top-level route to be rendered. For example, applying slug: false
topages/[[...slug]]
, will generate the page/
Fallback
- if
fallback
isfalse
, the missing path will be resolved by a404
page - if
fallback
istrue
, the behavior ofgetStaticProps
will be:-
- paths from
getStaticPaths
will be generated at build time usinggetStaticProps
- 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
- paths from
- 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 withgetServerSideProps
getStaticPaths
only runs on the server at build timegetStaticPaths
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 getStaticPropsreq:
HTTP IncomingMessage object (incoming message, request)res
: HTTP response objectquery
: object representation of the query stringpreview
: see getStaticPropspreviewData
: see getStaticPropsresolveUrl
: a normalized version of the requested URL, with the _next/data prefix removed and the original query string values includedlocale
: see getStaticPropslocales
: see getStaticPropsdefaultLocale
: see getStaticProps
getServerSideProps
should return an object with the following fields:
props
– seegetStaticProps
notFound
– seegetStaticProps
export async function getServerSideProps(context) { const res = await fetch('/data') const data = await res.json() if (!data) { return { notFound: true } } return { props: {} } }
redirect
— seegetStaticProps
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 serversidegetServerSideProps
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.