Next.js: A detailed guide

Dinesh Rawat
31 min readAug 17, 2022

--

Next.js is a React-based framework for developing web applications that have functionality that goes beyond SPA, i.e. so-called single-page applications.

As you know, the main disadvantage of SPA is the problems with indexing pages of such applications by search robots, which negatively affects SEO.

However, according to my personal observations, recently the situation has begun to change for the better, at least the pages of my small SPA-PWA application are indexed normally.

In addition, there are special tools such as react-snap that allow you to turn React-SPA into a multi-pager by pre-rendering the application to static markup. Meta-information can also be embedded in the head using utilities such as react-helmet. However, Next.js greatly simplifies the process of developing multi-page and hybrid applications (the latter cannot be achieved using the same react-snap). It also provides many other interesting features.

Please note that this article assumes that you have some experience with React. Also note that the notes will not make you an expert on Next.js, but will give you a comprehensive understanding of it.

Getting Started

To create a project, it is recommended to use create-next-app:

yarn create next-app app-name # typescript 
yarn create next-app app-name --typescript

Manual installation:

  • Install dependencies
yarn add next react react-dom
  • Update package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

Launching the development server:

yarn dev

Key features

Pages

A page is a React component that is exported from a .js, .jsx, .ts, or .tsx file located in the pages directory. Each page is associated with a route (route) by name. For example, pages/about.js would be available at /about. Please note that the page should be exported by default (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, and so on.

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

There are 2 forms of pre-rendering: static generation (SSG, the recommended approach) and server-side rendering (SSR). The first form involves generating HTML at build time and reusing it on every request. The second is the generation of markup on each request. Static markup generation is the recommended approach for performance reasons.

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

SSG

Both pages with data and pages without data can be generated.

No data

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

With data

There are 2 possible scenarios where you might want to generate a static data page:

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

Page content depends on external data

Let’s assume that the blog page receives a list of posts from the CMS:

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

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

export default function Blog({ posts }) {
// ...
}
export async function getStaticProps() {
const posts = await (await fetch('https://example.com/posts'))?.json()
return {
props: {
posts
}
}
}

Page paths depend on external data

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

export default function Post({ post }) {
// ...
}
export async function getStaticPaths() {
const posts = await (await fetch('https://example.com/posts'))?.json()
// pay attention to the structure of the returned array
const paths = posts.map((post) => ({
params: { id: post.id }
}))
// `fallback: false` means a different route is used for the 404 error
return {
paths,
fallback: false
}
}

The pages/posts/[id].js page should also export the getStaticProps function to get the data for the post with the given id:

export default function Post({ post }) {
// ...
}
export async function getStaticPaths() {
// ...
}
export async function getStaticProps({ params }) {
const post = await (await fetch(`https://example.com/posts/${params.id}`)).json()
return {
props: {
post
}
}
}

SSR

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

function Page({ data }) {
// ...
}
export async function getServerSideProps() {
const data = await (await fetch('https://example.com/data'))?.json()
return {
props: {
data
}
}
}

Data Fetching

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

  • getStaticProps (SSG): Get 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 asynchronous getStaticProps function is exported 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 would be { id: … }
  • previewtrue if the page is in preview mode
  • previewData — dataset set with 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 on 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 the fallback: false mode, since only the paths returned by getStaticPaths are pre-rendered in this mode.

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

  • redirect - is an optional object that allows you to perform redirects to internal and external resources, which must be of 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 currently not allowed. Such redirects should be added to next.config.js.

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

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

Use cases

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

Use with TypeScript

import { GetStaticProps } from 'next'  
export const getStaticProps: GetStaticProps = async (context) => {}

To get the expected types for props, 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 of type `Post[]`
}

Incremental static regeneration

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

Example:

const Blog = ({ posts }) => (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
)
// This function is called at build time on the 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 to regenerate the page:
// - when a new request arrives
// - at least once every 10 seconds
revalidate: 10 // in seconds
}
}
// This function is called at build time on the server.
// It can be called repeatedly as a serverless function
// if the path was not pre-generated
export async function getStaticPaths() {
const res = await fetch('/posts')
const posts = await res.json()
// Get post-based pre-render paths
const paths = posts.map((post) => ({
params: { id: post.id }
}))
// Only these paths will be pre-rendered at build time
// `{ fallback: 'blocking' }` will render pages on the server
// if there is no matching 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 the expiration of 10 seconds is also instantly returned from the cache
  • After 10 seconds, the next request also receives a cached version of the page in response
  • After that, page regeneration starts in the background.
  • After a successful refresh, the cache is invalidated and a new page is displayed. If the regeneration fails, the old page remains unchanged.

Reading files

To get the absolute path to the current working directory, use process.cwd():

import { promises as fs } from 'fs'
import { join } from 'path'
const Blog = ({ posts }) => (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h3>{post.filename}</h3>
<p>{post.content}</p>
</li>
))}
</ul>
)
// This function is called on the server, so
// in it you can directly access the database
export async function getStaticProps() {
const postsDir = join(process.cwd(), 'posts')
const filenames = await fs.readdir(postsDir)
const posts = filenames.map(async (filename) => {
const filePath = join(postsDir, filename)
const fileContent = await fs.readFile(filePath, 'utf-8')

// Usually, this is where the content is converted,
// e.g. parsing `markdown` to `HTML`
return {
filename,
content: fileContent
}
})
return {
props: {
posts: await Promise.all(posts)
}
}
}
export default Blog

Technical details

  • Because getStaticProps is run at build time, it cannot use data from the request, such as query string parameters (query params) or HTTP headers (headers)
  • getStaticProps only runs on the server, so it cannot be used to access internal routes
  • using getStaticProps generates not only HTML, but also a JSON file. This file contains the results of getStaticProps and is used by the client-side routing engine 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 each request

getStaticPaths

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

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

paths key

paths specifies which paths will be pre-rendered. For example, if we have a dynamically routed page called pages/posts/[id].js, and exported on that page, getStaticPaths returns paths like this:

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 hook, such as pages/[…slug], params must contain the slug as an array. For example, if such an array looks like [‘foo’, ‘bar’], then the page /foo/bar will be generated
  • if the page uses an optional route hook, 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 key

  • if fallback is false, the missing path will resolve to a 404 page
  • if fallback is true, getStaticProps will behave like this:
    1. paths from getStaticPaths will be generated at build time using getStaticProps
    2. the missing path will not be resolved by the 404 page. Instead, a fallback page will be returned in response to the request
    3. the requested HTML and JSON are generated in the background. This includes calling getStaticProps
    4. 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 point of view, it looks like switching between the fallback and full pages.
    5. the new path is added to the list of pre-rendered pages

Note that fallback: true is not supported when using next export.

Backup pages

In the backup version of the page:

  • page props 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()
// If the page hasn't been generated yet, this will be displayed
// Until `getStaticProps` is done
if (router.isFallback) {
return <div>Загрузка...</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 can fallback: true be useful?

fallback: true can be useful if you have a very large number of static pages that depend on data (for example, a very large online store). We want to pre-render all pages, but we understand that 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 watch the loading indicator for a while (while getStaticProps does its thing), then see the page itself. And after that, a new page will be returned in response to each request.

Note that fallback: true does not refresh the generated pages. For this, incremental static regeneration is used.

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 some waiting, will immediately see the finished page.

Use cases for getStaticPaths

getStaticPaths is used to pre-render pages with dynamic routing.

Use with TypeScript

import { GetStaticPaths } from 'next'
export const getStaticPaths: GetStaticPaths = async () => {}

Technical details

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

getServerSideProps

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

export async function getServerSideProps(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 would be { id: … }
  • req: HTTP IncomingMessage object (incoming message, request)
  • res: HTTP response object
  • query: object representation of the query string
  • preview: true if the page is in preview mode
  • previewData: dataset set with setPreviewData
  • resolveUrl: a normalized version of the requested URL, with the _next/data prefix removed and the values ​​of the original query string included
  • locale: current locale (if enabled)
  • locales: supported locales (if enabled)
  • defaultLocale: default locale (if enabled)

getServerSideProps should return an object with these fields:

  • props — optional object with props for the page
  • notFound — is an optional boolean value that allows you to return a 404 status and the corresponding page, for example:
  if (!data) {
return {
notFound: true
}
}
return {
props: {}
}
}

redirect — is an optional object that allows you to perform redirects to internal and external resources, which must be of the form { destination: string, permanent: boolean }:

export async function getServerSideProps(context) {
const res = await fetch('/data')
const data = await res.json()
if (!data) {
return {
redirect: {
destination: '/',
permanent: false
}
}
}
return {
props: {}
}
}

getServerSideProps has the same features and limitations as getStaticProps.

GetServerSideProps Use Cases

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

Use with TypeScript

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

To get the intended types for props, 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 details

  • getServerSideProps only run on the server
  • getServerSideProps can only be exported in a page component

Getting data on the client side

If the page has frequently updated data, but the page does not need to be pre-rendered (for SEO reasons), then it is possible to request such data on the client side.

The Next.js team recommends using the useSWR hook they developed for this, which provides features such as data caching, cache invalidation, focus tracking, periodic requeries, 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>An error occurred while loading data</div>
if (!data) return <div>Loading...</div>
return <div>Hi, {data.name}!</div>
}

Native CSS support

Import global styles

To add global styles, the corresponding table should be imported into the pages/_app.js file (note the underscore):

// pages/_app.js
import './style.css'
// This export is required by default
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}

Such styles will be applied to all pages and components in the application. Note that global styles can only be imported in pages/_app.js to avoid conflicts.

When the application is built, all styles are combined into one minified CSS file.

Import styles from the node_modules directory

Styles can be imported from node_modules.

An example of importing global styles:

// pages/_app.js
import 'bootstrap/dist/css/bootstrap.min.css'
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}

An example of importing styles for a third-party component:

// components/Dialog.js
import { useState } from 'react'
import { Dialog } from '@reach/dialog'
import VisuallyHidden from '@reach/visually-hidden'
import '@reach/dialog/styles.css'
export function MyDialog(props) {
const [show, setShow] = useState(false)
const open = () => setShow(true)
const close = () => setShow(false)
return (
<div>
<button onClick={open} className='btn-open'>Open</button>
<Dialog>
<button onClick={close} className='btn-close'>
<VisuallyHidden>Close</VisuallyHidden>
<span>X</span>
</button>
<p>Hi!</p>
</Dialog>
</div>
)
}

Adding Styles at the Component Level

Next.js supports CSS modules out of the box. CSS modules must be named [name].module.css. They create a local scope for the respective styles, which allows the same class names to be used without the risk of collisions. A CSS module is imported as an object (usually named styles) whose keys are the names of the respective classes.

An example of using CSS modules:

/* components/Button/Button.module.css */
.danger {
background-color: red;
color: white;
}
// components/Button/Button.js
import styles from './Button.module.css'
export const Button = () => (
<button className={styles.danger}>
Delete
</button>
)

When built, CSS modules are concatenated and separated into separate minified CSS files, allowing only the required styles to be loaded.

SASS support

Next.js supports .scss and .sass files. SASS can also be used at the component level (.module.scss and .module.sass). To compile SASS to CSS, you need to install sass:

yarn add sass

The behavior of the SASS compiler can be customized in the next.config.js file, for example:

const path = require('path')module.exports = {
sassOptions: {
includePaths: [path.join(__dirname, 'styles')]
}
}

CSS-in-JS

Any CSS-in-JS solution can be used in Next.js. The simplest example is using inline styles:

export const Hi = ({ name }) => <p style={{ color: 'green' }}>Hi {name}!</p>

Next.js also has native styled-jsx support:

export const Bye = ({ name }) => (
<div>
<p>Bye, {name}. See you soon!</p>
<style jsx>{`
div {
background-color: #3c3c3c;
}
p {
color: #f0f0f0;
}
@media(max-width: 768px) {
div {
backround-color: #f0f0f0;
}
p {
color: #3c3c3c;
}
}
`}</style>
<style global jsx>{`
body {
margin: 0;
min-height: 100vh;
display:grid;
place-items: center;
}
`}</style>
</div>
)

Layouts

Developing a React application involves dividing the page into separate components. Many components are used on multiple pages. Let’s assume that each page has a navigation bar and a footer:

// components/layout.js
import Navbar from './navbar'
import Footer from './footer'
export default function Layout({ children }) {
return (
<>
<Navbar />
<main>{children}</main>
<Footer />
</>
)
}

Examples

Single Layout

If the application only uses one layout, we can create a custom application (custom app) and wrap the application in a layout. Since the layout component will be reused when pages change, its state (for example, input values) will be preserved:

// pages/_app.js
import Layout from '../components/layout'
export default function App({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}

Page Level Layouts

The page’s getLayout property allows you to return a layout component. This allows you to define layouts at the page level. The return function allows you to construct nested layouts:

// pages/index.js
import Layout from '../components/layout'
import Nested from '../components/nested'
export default function Page() {
return {
// ...
}
}
Page.getLayout = (page) => (
<layout>
<Nested>{page}</Nested>
</Layout>
)
// pages/_app.js
export default function App({ Component, pageProps }) {
// use the layout defined at the page level, if any
const getLayout = Component.getLayout || ((page) => page)
return getLayout(<Component {...pageProps} />)
}

When switching pages, the state of each of them (input values, scroll position, etc.) will be saved.

Use with TypeScript

When using TypeScript, a new type is first created for the page that includes getLayout. Next, create a new type for AppProps that overrides the Component property to make use of the previously created type:

// pages/index.tsx
import type { ReactElement } from 'react'
import Layout from '../components/layout'
import Nested from '../components/nested'
export default function Page() {
return {
// ...
}
}
Page.getLayout = (page: ReactElement) => (
<layout>
<Nested>{page}</Nested>
</Layout>
)
// pages/_app.tsx
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page)
return getLayout(<Component {...pageProps} />)
}

Getting data

Layout data can be retrieved on the client side using useEffect or utilities like SWR. Because a layout is not a page, you cannot currently use getStaticProps or getServerSideProps on it:

import useSWR from 'swr'
import Navbar from './navbar'
import Footer from './footer'
export default function Layout({ children }) {
const { data, error } = useSWR('/data', fetcher)
if (error) return <div>Error</div>
if (!data) return <div>Loading...</div>
return (
<>
<navbar />
<main>{children}</main>
<Footer />
</>
)
}

The Image Component and Image Optimization

The Image component imported from next/image is an extension of the HTML img tag for the modern web. It includes several built-in optimizations to achieve good Core Web Vitals performance. These optimizations include the following:

  • performance improvement
  • ensuring visual stability
  • page load speedup
  • providing flexibility (scalability) of images

Example of using a local image

import Image from 'next/image'
import imgSrc from '../public/some-image.png'
export default function Home() {
return (
<>
<h1>Main Page</h1>
<Image
src={imgSrc}
alt=""
role="presentation"
/>
</h1>
)
}

Remote image example

Note the need to set the width and height of the image:

import Image from 'next/image'export default function Home() {
return (
<>
<h1>Home</h1>
<Image
src="/some-image.png"
alt=""
role="presentation"
width={500}
height={500}
/>
</h1>
)
}

Image Sizing

Image expects to get the width and height of the image:

  • In case of static import (local image) width and height are calculated automatically
  • Width and height can be specified with appropriate props
  • if the dimensions of the image are unknown, you can use the layout prop with a value of fill

There are 3 ways to solve the problem of unknown image sizes:

  1. Using the fill layout mode: This mode allows you to control the dimensions of the image using the parent element. In this case, the dimensions of the parent element are determined using CSS, and the dimensions of the image are determined using the object-fit and object-position properties.
  2. Image normalization: if the image source is under our control, we can add image resizing when it is returned in response to a request
  3. Modification of calls to the API: in response to a request, not only the image itself, but also its dimensions can be included

Image Styling Rules

  • Choose the right layout mode
  • Use className — it is set to the corresponding img element. Note: the style prop is not passed
  • When using layout=”fill” the parent element must have position: relative
  • When using layout=”responsive” the parent element must have display: block

See below for more details on the Image component.

Font optimization

Next.js automatically embeds fonts in CSS at build time:

// It was
<link
href="https://fonts.googleapis.com/css2?family=Inter"
rel="stylesheet"
/>
// became
<style data-href="https://fonts.googleapis.com/css2?family=Inter">
@font-face{font-family:'Inter';font-style:normal...}
</style>

To add a font to the page, use the Head component imported from next/head:

// pages/index.js
import Head from 'next/head'
export default function IndexPage() {
return(
<div>
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter&display=optional"
/>
</Head>
<p>Hi folks!</p>
</div>
)
}

To add a font to an application, create a custom document:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDoc extends Document {
render() {
return (
<html>
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter&display=optional"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</HTML>
)
}
}

Automatic font optimization can be disabled:

// next.config.js
module.exports = {
optimizeFonts: false
}

See below for more details on the Head component.

Script Component

The Script component allows developers to prioritize the loading of third-party scripts, which saves time and improves performance.

The script loading priority is determined using the strategy prop, which takes one of the following values:

  • beforeInteractive: Is for important scripts that must be loaded and executed before the page becomes interactive. Such scripts include, for example, detecting bots and requesting permissions. Such scripts are embedded in the initial HTML and run before the rest of the JS.
  • afterInteractive: For scripts that can be loaded and executed after the page has become interactive. Examples of such scripts include tag managers and analytics. Such scripts are executed on the client side and run after hydration
  • lazyOnload: For scripts that can be loaded during the idle period. Such scripts include, for example, chat support and social media widgets.

Note:

  • Script supports inline scripts with afterInteractive and lazyOnload strategies
  • Inline scripts wrapped in Script must have an id attribute to track and optimize them

Examples

Note that the Script component must not be placed inside a Head component or a custom document.

Loading polyfills

import Script from 'next/script'export default function Home() {
return (
<>
<Script
src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserverEntry%2CIntersectionObserver"
strategy="beforeInteractive"
/>
</>
)
}

Lazy loading

import Script from 'next/script'export default function Home() {
return (
<>
<Script
src="https://connect.facebook.net/en_US/sdk.js"
strategy="lazyOnload"
/>
</>
)
}

Executing code after the page is fully loaded

import { useState } from 'react'
import Script from 'next/script'
export default function Home() {
const [stripe, setStripe] = useState(null)
return (
<>
<Script
id="stripe-js"
src="https://js.stripe.com/v3/"
onLoad={() => {
setStripe({ stripe: window.Stripe('pk_test_12345') })
}}
/>
</>
)
}

Built-in scripts

import Script from 'next/script'<Script id="show-banner" strategy="lazyOnload">
{`document.getElementById('banner').classList.remove('hidden')`}
</Script>
// or
<Script
id="show banner"
dangerouslySetInnerHTML={{
__html: `document.getElementById('banner').classList.remove('hidden')`
}}
/>

Passing Attributes

import Script from 'next/script'export default function Home() {
return (
<>
<Script
src="https://www.google-analytics.com/analytics.js"
id="analytics"
nonce="XUENAJFW"
data-test="analytics"
/>
</>
)
}

Serving static files

Static resources must be placed in the public directory located in the project root directory. Files located in the public directory are accessible via the base link /:

import Image from 'next/image'export default function Avatar() {
return <Image src="/me.png" alt="me" width="64" height="64" >
}

This directory is also great for storing files like robots.txt, favicon.png, files needed for Google site verification and other statics (including .html).

Real time update

Next.js supports updating components in real time with local state in most cases (this only applies to functional components and hooks). The state of the component is also preserved when errors (not related to rendering) occur.

To reload a component, just add // @refresh reset anywhere.

Typescript

Next.js supports TypeScript out of the box:

yarn create next-app --typescript app-name
# or
yarn create next-app --ts app-name

For getStaticProps , getStaticPaths and getServerSideProps there are special types GetStaticProps , GetStaticPaths and GetServerSideProps :

import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next'export const getStaticProps: GetStaticProps = async(context) => {
// ...
}
export const getStaticPaths: GetStaticPaths = async() => {
// ...
}
export const getServerSideProps: GetServerSideProps = async(context) => {
// ...
}

An example of using built-in types for the routing interface (API Routes):

import type { NextApiRequest, NextApiResponse } from 'next'export default (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ message: 'Hi!' })
}

Nothing prevents us from typing the data contained in the response:

import type { NextApiRequest, NextApiResponse } from 'next'type Data = {
name:string
}
export default (req: NextApiRequest, res: NextApiResponse<Data>) => {
res.status(200).json({ message: 'Bye!' })
}

For a custom application, there is a special type AppProps:

// import App from 'next/app'
import type { AppProps /*, AppContext */ } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
// This method should only be used when all application pages
// must be pre-rendered on the server
MyApp.getInitialProps = async(context: AppContext) => {
// calls the `getInitialProps` function defined on the page
// and populate `appProps.pageProps`
const props = await App.getInitialProps(context)
return { ...props }
}

Next.js supports the paths and baseUrl settings defined in tsconfig.json.

Environment variables

Next.js has built-in support for environment variables, which allows you to do the following:

  • Use .env.local to load variables
  • Extrapolate variables to browser using NEXT_PUBLIC_ prefix

Let’s say we have a .env.local file like this:

DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword

This will automatically load process.env.DB_HOST, process.env.DB_USER, and process.env.DB_PASS into the Node.js runtime, allowing them to be used in getter methods and the routing interface:

// pages/index.js
export async function getStaticProps() {
const db = await myDB.connect({
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASS
})
// ...
}

Next.js allows you to use variables inside .env files:

HOSTNAME=localhost
PORT=8080
HOST=http://$HOSTNAME:$PORT

In order to pass an environment variable to the browser, you need to add the NEXT_PUBLIC_ prefix to it:

NEXT_PUBLIC_ANALYTICS_ID=abcdefghijk// pages/index.js
import setupAnalyticsService from '../lib/my-analytics-service'
setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID)functionHomePage() {
return <h1>Hi folks!</h1>
}
export default HomePage

In addition to .env.local, you can create .env (for both modes), .env.development (for development mode), and .env.production (for production mode) files. Note that .env.local always takes precedence over other files containing environment variables.

Routing

Introduction

Routing in Next.js is based on the concept of pages.

A file placed in the pages directory automatically becomes a route.

The index.js files are linked to the root directory:

  • pages/index.js -> /
  • pages/blog/index.js -> /blog

The router supports nested files:

  • pages/blog/first-post.js -> /blog/first-post
  • pages/dashboard/settings/username.js -> /dashboard/settings/username

Dynamic route segments are defined using square brackets:

  • pages/blog/[slug].js -> /blog/:slug (blog/first-post)
  • pages/[username]/settings.js -> /:username/settings (/johnsmith/settings)
  • pages/post/[…all].js -> /post/* (/post/2021/id/title)

Relationship between pages

The Link component is used for client-side routing:

import Link from 'next/link'export default function Home() {
return (
<ul>
<li>
<Link href="/">
home
</Link>
</li>
<li>
<Link href="/about">
About Us
</Link>
</li>
<li>
<Link href="/blog/first-post">
Post number one
</Link>
</li>
</ul>
)
}

Here:

  • / → pages/index.js
  • /about → pages/about.js
  • /blog/first-post → pages/blog/[slug].js

For dynamic segments, you can use interpolation:

import Link from 'next/link'export default function Post({ posts }) {
return(
<ul>
{posts.map((post) => (
<likey={post.id}>
<Link href={`/blog/${encodeURIComponent(post.slug)}`}>
{post.title}
</Link>
</li>
))}
</ul>
)
}

Or a URL object:

import Link from 'next/link'export default function Post({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link
href={{
pathname: '/blog/[slug]',
query: { slug: post.slug },
}}
>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>
)
}

Here:

  • pathname is the name of the page in the pages directory (/blog/[slug] in this case)
  • query is an object with a dynamic segment (slug in this case)

You can use the useRouter hook or the withRouter utility to access the router object in the component. It is recommended to use useRouter.

Dynamic routes

To create a dynamic route, add [param] to the page path.

Consider page pages/post/[pid].js:

import { useRouter } from 'next/router'export default function Post() {
const router = useRouter()
const { pid } = router.query
return <p>Post: {pid}</p>
}

Routes /post/1, /post/abc, etc. will match pages/post/[pid].js. The matched parameter is passed to the page as a query string parameter, concatenated with other parameters.

For example, for the /post/abc route, the query object would look like this:

{ "pid": "abc" }

And for the /post/abc?foo=bar route:

{ "pid": "abc", "foo": "bar" }

The route parameters overwrite the query string parameters, so the query object for the /post/abc?pid=123 route would look like this:

{ "pid": "abc" }

For routes with several dynamic segments, the query is formed in the same way. For example, the page pages/post/[pid]/[cid].js would match the route /post/123/456 and the query would look like this:

{ "pid": "123", "cid": "456" }

Navigation between dynamic routes on the client side is handled with next/link:

import Link from 'next/link'export default function Home() {
return (
<ul>
<li>
<Link href="/post/abc">
Leads to the page `pages/post/[pid].js`
</Link>
</li>
<li>
<Link href="/post/abc?foo=bar">
Also leads to the page `pages/post/[pid].js`
</Link>
</li>
<li>
<Link href="/post/123/456">
<a>Goes to `pages/post/[pid]/[cid].js`</a>
</Link>
</li>
</ul>
)
}

Interception of all paths

Dynamic routes can be extended to capture all paths by adding an ellipsis (…) in square brackets. For example pages/post/[…slug].js will match /post/a, /post/a/b, /post/a/b/c etc.

Note that instead of slug, you can use any other name, for example, […param].

The matched parameters are passed to the page as query string parameters (slug in this case) with the value as an array. For example, a query for /post/a would be of the form:

{ "slug": ["a"] }

And for /post/a/b this:

{ "slug": ["a", "b"] }

Routes for intercepting all paths can be optional — for this, the parameter must be wrapped in one more square brackets ([[…slug]]).

For example, pages/post/[[…slug]].js will match /post, /post/a, /post/a/b, etc.

The main difference between regular and optional hooks is that an optional hook has a route with no parameters (/post in our case).

Examples of the query object:

{ } // GET `/post` (empty object)
{ "slug": ["a"] } // `GET /post/a` (array with one element)
{ "slug": ["a", "b"] } // `GET /post/a/b` (array with multiple elements)

Pay attention to the following features:

  • static routes take precedence over dynamic routes, and dynamic routes take precedence over hooks, for example:
    1. pages/post/create.js — will match /post/create
    2. pages/post/[pid].js will match /post/1, /post/abc, etc., but not /post/create
    3. pages/post/[…slug].js will match /post/1/2, /post/a/b/c etc., but not /post/create and /post/abc
  • pages rendered with automatic static optimization will be hydrated without route parameters, i.e. query will be an empty object ({}). After hydration, an app update will be triggered to populate the query

An imperative approach to client-side navigation

In most cases, the Link component from next/link is enough to implement client-side navigation. However, you can also use the router from next/router for this:

import { useRouter } from 'next/router'export default function ReadMore() {
const router = useRouter()
return(
<button onClick={() => router.push('/about')}>
Read more
</button>
)
}

Shallow Routing

Silent routing allows you to change the URL without restarting methods to get data, including the getServerSideProps and getStaticProps functions.

We get the updated pathname and query through the router object (obtained with useRouter() or withRouter() ) without losing the state of the component.

To enable silent routing, use the { shallow: true } setting:

import { useEffect } from 'react'
import { useRouter } from 'next/router'
// current `URL` is `/`
export default function Page() {
const router = useRouter()
useEffect(() => {
// do navigation after first render
router.push('?counter=1', undefined, { shallow: true })
}, [])
useEffect(() => {
// value of `counter` has changed!
}, [router.query.counter])
}

When you update the URL, only the state of the route will change.

Please note: silent routing only works within the same page. Let’s say we have a pages/about.js page and we do the following:

router.push(‘?counter=1’, ‘/about?counter=1’, { shallow: true })

In this case, the current page is unloaded, a new one is loaded, methods for getting data are restarted (despite the presence of { shallow: true }).

Routing Interface (API Routes)

Introduction

Any file in the pages/api directory is mapped to /api/* and is considered an API endpoint, not a page. The routing interface code remains on the server and does not affect the size of the client assembly.

The following example of the pages/api/user.js route returns a status code of 200 and data in JSON format:

export default function handler(req, res) {
res.status(200).json({ name: 'Dinesh Rawat' })
}

The handler function takes two parameters:

  • req — an instance of http.IncomingMessage + some built-in middlewares (see below)
  • res — is an instance of http.ServerResponse + some helper functions (see below)

You can use req.method to handle different methods:

export default function handler(req, res) {
if (req.method === 'POST') {
// work with POST request
} else {
// work with another request
}
}

Use cases

In a new project, the entire API can be built using the routing interface. The existing API does not need to be updated. Other cases:

  • external service URL masking
  • using environment variables stored on the server to securely access external services

Peculiarities

  • The routing interface does not define CORS headers by default. This is done through middlewares (see below)
  • routing interface cannot be used with next export

As for dynamic segments in routing interface routes, they follow the same rules as the dynamic parts of page routes.

Middlewares

The routing interface includes the following middlewares that transform the incoming request (req):

  • req.cookies — an object containing the cookies included in the request (the default value is {})
  • req.query — an object containing the query string (the default value is {})
  • req.body — an object containing the request body converted based on the Content-Type header, or null

Customization of middlewares

Each route can export a config object with settings for proxies:

export const config = {
API: {
bodyParser: {
sizeLimit: '1mb'
}
}
}
  • bodyParser: false — disables response parsing (raw data stream is returned — Stream)
  • bodyParser.sizeLimit — the maximum size of the request body in any format supported by bytes
  • externalResolver: true — tells the server that this route is being processed by an external resolver such as express or connect

Adding middleware

Consider adding a cors middleware.

Installing the module:

yarn add cors

Add cors to the route:

import Cors from 'cors'// initialize the middleware
const cors = Cors({
methods: ['GET', 'HEAD']
})
// helper function to wait for successful mediation resolution
// before executing other code
// or to throw an exception when an error occurs in the broker
const runMiddleware = (req, res, next) =>
new Promise((resolve, reject) => {
fn(req, res, (result) =>
result instanceof Error ? reject(result) : resolve(result)
)
})
export default async function handler(req, res) {
// start the broker
await runMiddleware(req, res, cors)
// rest of `API` logic
res.json({ message: 'Hello everyone!' })
}

Secondary functions

The response (res) object includes a set of methods to improve the development experience and speed up the creation of new endpoints.

This set includes the following:

  • res.status(code) — a function to set the status code of the response
  • res.json(body) — to send a response in JSON format, body — any serializable object
  • res.send(body) — to send a response, body — string, object or Buffer
  • res.redirect([status,] path) — to redirect to the specified page, default status is 307 (temporary redirect)

Preparing for production

  • Use caching (see below) wherever possible
  • Make sure the server and database are (deployed) in the same region
  • Minimize the amount of JavaScript code
  • Delay loading “heavy” JS until it’s actually used
  • Make sure the logging is set up correctly
  • Make sure error handling is correct
  • Configure 500 (server error) and 404 (page missing) pages
  • Make sure the application meets the best performance criteria
  • Run Lighthouse to test performance, best practices, accessibility, and search engine optimization. Use a production build and incognito mode in the browser so that nothing outside influences the results
  • Make sure that the features used in your application are supported by modern browsers
  • For better performance use the following:
    1. next/image and automatic image optimization
    2. automatic font optimization
    3. script optimization

Caching

Caching reduces the response time and the number of requests to external services. Next.js automatically adds caching headers to immutable resources delivered from _next/static, including JS, CSS, images, and other media.

Cache-Control: public, max-age=31536000, immutable

To revalidate the cache of a page that has been previously rendered to static markup, use the revalidate setting in the getStaticProps function.

Note that running the app in development mode with next dev disables caching.

Cache-Control: no-cache, no-store, max-age=0, must-revalidate

Caching headers can also be used in getServerSideProps and the routing interface for dynamic responses. An example of using stale-while-revalidate:

// Value is considered fresh for 10 seconds (s-maxage=10).
// If the request is repeated within 10 seconds, the previous cached value
// considered fresh. If the request is repeated within 59 seconds,
// cached value is considered obsolete, but still used for rendering
// (stale-while-revalidate=59)
// After that, the request is executed in the background and the cache is filled with fresh data.
// After refresh, the page will display the new value
export async function getServerSideProps({ req, res }) {
res.setHeader(
'Cache Control',
'public, s-maxage=10, stale-while-revalidate=59'
)
return {
props: {}
}
}

Reducing the amount of JavaScript used

To determine what is included in each JS bundle, you can use the following tools:

  • Import Cost — is an extension for VSCode that shows the size of the imported package
  • Package Phobia — a service for determining the “cost” of adding a new development dependency to the project (dev dependency)
  • Bundle Phobia — a service for determining how much adding a dependency will increase the size of the assembly
  • Webpack Bundle Analyzer — A webpack plugin for visualizing a bundle as an interactive, scalable tree structure

Each file in the pages directory is allocated to a separate build during the next build command. You can use dynamic import to lazy load components and libraries.

Authentication

Authentication is the process of determining who the user is, and authorization is the process of determining his authority, i.e. what the user has access to. Next.js supports several authentication patterns.

Authentication patterns

The authentication pattern defines the strategy for obtaining data. Next, you need to select an authentication provider that supports the selected strategy. There are two main authentication patterns:

  1. using static generation for server loading state and getting user data on client side
  2. receiving user data from the server to avoid a “flush” of unauthenticated content (meaning user-visible application state switching)

Static Generation Authentication

Next.js automatically determines that a page is static if the page does not have any blocking methods for getting data. This means there is no getServerSideProps on the page. In this case, the initial state received from the server is rendered on the page, and then the user data is requested on the client side.

One of the advantages of using this pattern is the ability to deliver pages from a global CDN and preload them with next/link. This leads to a decrease in time to interactivity (Time to Interactive, TTI).

Consider an example of a user profile page. On this page, the template (skeleton) is first rendered, and after a request is made to obtain user data, this data is displayed:

// pages/profile.js
import useUser from '../lib/useUser'
import Layout from './components/Layout'export default function Profile() {
// get user data on the client side
const { user } = useUser({ redirectTo: '/login' }) // loading status received from the server
if (!user || user.isLoggedIn === false) {
return <Layout>Loading...</Layout>
} // after executing the request, the user data is displayed
return(
<layout>
<h1>Your profile</h1>
<pre>{JSON.stringify(user, null, 2)}</pre>
</Layout>
)
}

Server Side Rendering Authentication

If the page has an asynchronous getServerSideProps function, Next.js will render that page on every request using the data from this function.

export async function getServerSideProps(context) {
return {
props: {} // will be passed to the page component as props
}
}

Let’s rewrite the above example. If there is a session, the Profile component will get the user prop. Notice the lack of a template:

// pages/profile.js
import withSession from '../lib/session'
import Layout from '../components/Layout'export const getServerSideProps = withSession(async (req, res) => {
const user = req.session.get('user') if (!user) {
return {
redirect: {
destination: '/login',
permanent: false
}
}
} return {
props: {
user
}
}
})export default function Profile({ user }) {
// display user data, no loading state needed
return (
<layout>
<h1>Your profile</h1>
<pre>{JSON.stringify(user, null, 2)}</pre>
</Layout>
)
}

This approach has the advantage of preventing a flash of unauthenticated content before the redirect is performed. It’s important to note that requesting user data in getServerSideProps blocks rendering until the request is resolved. Therefore, in order to avoid creating bottlenecks and increasing the time to the first byte (Time to Fist Byte, TTFB), you should make sure that the authentication service performs well.

Authentication providers

If you have a database with users, consider using one of the following solutions:

  • next-iron-session — low-level encoded stateless session
  • next-auth is a complete authentication system with built-in providers (Google, Facebook, GitHub, etc.), JWT, JWE, email/password, magic links, etc.

If you prefer the good old passport:

Perhaps that’s all for today.

Thank you for your attention and have a nice day!

--

--