NextJS

Setup

As of march 2021 you need node version 10.13 or later to run NextJS. Create a folder an initialize your project with

npx create-next-app my-app-name
cd my-app-name

yarn dev   - Starts development server on localhost:3000
yarn build - builds the application for production usage
yarn start - starts a Next.js production server

What we will get is

Pages and Routing

In Next.js, a page is a React Component exported from a file (.js, .jsx, .ts, or .tsx) in the pages directory. The component can have any name, but you must export it as a default export.

  • pages/index.js is associated with the / route.
  • pages/posts/first-post.js is associated with the /posts/first-post route.

To match a dynamic segment you can use the bracket-syntax. This allows you to match named parameters:

  • pages/blog/[slug].js/blog/:slug (/blog/hello-world)
  • pages/[username]/settings.js/:username/settings (/foo/settings)
  • pages/post/[...all].js/post/* (/post/2020/id/title)

See ‘Statically Generated Pages with Dynamic Routes’ below to learn more.

Client-Side Navigation with <Link>

You use <a> tag and wrap them in a Link Component

import Link from 'next/link'

export default function FirstPost() {
  return (
    <>
      <h1>First Post</h1>
      <h2>
        <Link href="/">
          <a>Back to home</a>
        </Link>
      </h2>
    </>
  )
}

Of course, you can also interpolate your links:

import Link from 'next/link'

function Posts({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
            <a>{post.title}</a>
          </Link>
        </li>
      ))}
    </ul>
  )
}

export default Posts

Or alternatively use a URL object:

<Link
  href={{
    pathname: '/blog/[slug]',
    query: { slug: post.slug },
  }}
>

In a production build of Next.js, whenever Link components appear in the browser’s viewport, Next.js automatically prefetches the code for the linked page in the background. By the time you click the link, the code for the destination page will already be loaded in the background, and the page transition will be near-instant.

If you need to link to an external page outside the Next.js app, just use an <a> tag without Link. If you need to add attributes like, for example, className, add it to the a tag, not to the Link tag.

Linking imperatively

<Link> should be able to cover most of your routing needs, but you can also do client-side navigation imperatively:

import { useRouter } from 'next/router'

function ReadMore() {
  const router = useRouter()

  return (
    <span onClick={() => router.push('/about')}>Click here to read more</span>
  )
}

export default ReadMore

Change URL without fetching data again

Also called Shallow Routing. getServerSideProps, getStaticProps, and getInitialProps will not be called, but you will still receive the updated pathname and the query via the router object when you set the shallow option to true:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// Current URL is '/'
function Page() {
  const router = useRouter()

  useEffect(() => {
    // Always do navigations after the first render
    router.push('/?counter=10', undefined, { shallow: true })
  }, [])

  useEffect(() => {
    // The counter changed!
  }, [router.query.counter])
}

export default Page

Shallow routing only works for same-page URL changes.

Assets (such as images)

Files inside public can be referenced from the root of the application. That is useful to serve static assets like robots.txt and images. For example, if you add an image to public/me.png, the following code will access the image:

import Image from 'next/image'

function Avatar() {
  return <Image src="/me.png" alt="me" width="64" height="64" />
}

export default Avatar

Note: Be sure to not have a static file with the same name as a file in the pages/ directory, as this will result in an error.

Next.js provides an Image component out of the box to handle the following for you:

  • Ensuring your image is responsive on different screen sizes
  • Optimizing your images without the need of a third-party tool or library
  • Only loading images when they enter the viewport (Lazy loading)

Image Optimization

The Automatic Image Optimization (since version 10) allows for resizing, optimizing, and serving images in modern formats like WebP when the browser supports it. This avoids shipping large images to devices with a smaller viewport. It also allows Next.js to automatically adopt future image formats and serve them to browsers that support those formats.

Automatic Image Optimization works with any image source, even if the image is hosted by an external data source, like a CMS. Next.js optimizes images on-demand, as users request them, not at build time.

import Image from 'next/image'

const YourComponent = () => (
  <Image
    src="/images/profile.jpg" // Route of the image file
    height={144} // Desired size with correct aspect ratio
    width={144} // Desired size with correct aspect ratio
    alt="Your Name"
  />
)

Images are optimized dynamically upon request and stored in the <distDir>/cache/images directory. That means it does not work when using next export, which exports static files. The expiration (or rather Max Age) is defined by the upstream server’s Cache-Control header. You can optionally configure Image Optimization for more advanced use cases via next.config.js.

Image layout

The layout behavior of the image changes as the viewport changes size. Defaults to intrinsic. When fixed, the image dimensions will not change as the viewport changes (no responsiveness) similar to the native img element. When intrinsic, the image will scale the dimensions down for smaller viewports but maintain the original dimensions for larger viewports. When responsive, the image will scale the dimensions down for smaller viewports and scale up for larger viewports. When fill, the image will stretch both width and height to the dimensions of the parent element, usually paired with object-fit. Demos can be found here.

Image domains

To enable Image Optimization for images hosted on an external website, use an absolute url for the Image src and specify which domains are allowed to be optimized. This is needed to ensure that external urls can’t be abused. When loader is set to an external image service, this option is ignored.

module.exports = {
  images: {
    domains: ['example.com'],
  },
}

Metadata in <Head>

You change metadata by using <Head> Component instead of <head> element.

import Head from 'next/head'

export default function FirstPost() {
  return (
    <>
      <Head>
        <title>First Post</title>
      </Head>
      <h1>First Post</h1>
    </>
  )
}

CSS Styling

Next.js allows several possibilities for styling:

  • Built-in support for CSS and Sass which allows you to import .css and .scss files (still needs npm install sass)
  • Built-in support for CSS modules via import ./mycomponent.module.css which will automatically generate unique class names. Next.js’s code splitting feature works on CSS Modules as well. It ensures the minimal amount of CSS is loaded for each page.
  • CSS-in-JS libraries such as styled-jsx (built-in support), styled-components or emotion
  • Using popular CSS libraries like Tailwind CSS

To load global CSS files, create a file called pages/_app.js and import styles/global.css for example:

// pages/_app.js
import '../styles/global.css'
// also direct imports from node_modules are allowed
import 'bootstrap/dist/css/bootstrap.css'

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />
}

This App component is the top-level component which will be common across all the different pages. You can use this App component to keep state when navigating between pages, for example. You cannot import global CSS anywhere else.

Customizing PostCSS Config

Out of the box, with no configuration, Next.js compiles CSS using PostCSS. To customize PostCSS config, you can create a top-level file called postcss.config.js. This is useful if you’re using libraries like Tailwind CSS.

npm install tailwindcss postcss-preset-env postcss-flexbugs-fixes
// postcss.config.js

module.exports = {
  plugins: [
    'tailwindcss',
    'postcss-flexbugs-fixes',
    [
      'postcss-preset-env',
      {
        autoprefixer: {
          flexbox: 'no-2009'
        },
        stage: 3,
        features: {
          'custom-properties': false
        }
      }
    ]
  ]
}
// tailwind.config.js
module.exports = {
  purge: [
    // Use *.tsx if using TypeScript
    './pages/**/*.js',
    './components/**/*.js'
  ]
  // ...
}

Rendering techniques

By default, Next.js pre-renders every page using Static Generation without fetching data. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO.

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

Next.js has two forms of pre-rendering:

  • Static Site Generation (SSG) using getStaticProps() is the pre-rendering method that generates the HTML at build time. The pre-rendered HTML is then reused on each request.
  • Server-side Rendering (SSR) using getServerSideProps() is the pre-rendering method that generates the HTML on each request.

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

When to use which kind of rendering?

In development mode (when you run npm run dev or yarn dev), every page is pre-rendered on each request — even for pages that use Static Generation.

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

You should use getStaticProps if:

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

On the other hand, Static Generation is not a good idea if you cannot pre-render a page ahead of a user’s request: Maybe your page shows frequently updated data, and the page content changes on every request. You won’t be able to use data with static generation that’s only available during request time, such as query parameters or HTTP headers. In that case, you can use Server-side Rendering. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate frequently updated data.

Static Site Generation (SSG)

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

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

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

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

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

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

Server Side Rendering (SSR)

To use Server-side Rendering, you need to export getServerSideProps instead of getStaticProps from your page.

export async function getServerSideProps(context) {
  return {
    props: {
      // props for your component
    }
  }
}

Client-side Rendering

You first statically generate (pre-render) parts of the page that do not require external data. Then, when the page loads, you fetch external data from the client using JavaScript and populate the remaining parts. This approach works well for user dashboard pages, for example. Because a dashboard is a private, user-specific page, SEO is not relevant, and the page doesn’t need to be pre-rendered. The data is frequently updated, which requires request-time data fetching.

The team behind Next.js has created a React hook for data fetching called SWR. It is highly recommended if you’re fetching data on the client side. It handles caching, revalidation, focus tracking, refetching on interval, and more.

import useSWR from 'swr'

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

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

Statically Generated Pages with Dynamic Routes (getStaticPaths)

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

import Layout from '../../components/layout'

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

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

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

  return {
    paths,
    fallback: false // any paths not returned by getStaticPaths will result in a 404 page
  }
}

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

  return {
    props: {
      postData
    }
  }
}

You could access the router directly:

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

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

import { useRouter } from 'next/router'

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

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

export default Post

Catch-All Routes […slug]

Dynamic routes can be extended to catch all paths by adding three dots (...) inside the brackets: pages/post/[...slug].js matches /post/a, but also /post/a/b, /post/a/b/c and so on. The resulting query object would be:

// the file pages/post/[...slug].js

// if called as /post/a would result as query:
{ "slug": ["a"] }

// if called as /post/a/b would result as query:
{ "slug": ["a", "b"] }

Optional Catch-All Routes [[…slug]]

Catch all routes can be made optional by including the parameter in double brackets ([[...slug]]).

For example, pages/post/[[...slug]].js will match /post, /post/a, /post/a/b, and so on.

The main difference between catch all and optional catch all routes is that with optional, the route without the parameter is also matched. The query objects are as follows:

{ } // GET `/post` (empty object)
{ "slug": ["a"] } // `GET /post/a` (single-element array)
{ "slug": ["a", "b"] } // `GET /post/a/b` (multi-element array)

Note: Predefined routes take precedence over dynamic routes, and dynamic routes over catch all routes.

Fallback

If fallback is false, then any paths not returned by getStaticPaths will result in a 404 page. If fallback is true, then the behavior of getStaticProps changes:

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

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

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

  // Render post...
}

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

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // Pass post data to the page via props
  return {
    props: { post },
    // Re-generate the post at most once per second
    // if a request comes in
    revalidate: 1,
  }
}

export default Post

fallback: true is useful if your app has a very large number of static pages that depend on data (think: a very large e-commerce site). You want to pre-render all product pages, but then your builds would take forever.

Instead, you may statically generate a small subset of pages and use fallback: true for the rest. When someone requests a page that’s not generated yet, the user will see the page with a loading indicator. Shortly after, getStaticProps finishes and the page will be rendered with the requested data. From now on, everyone who requests the same page will get the statically pre-rendered page.

This ensures that users always have a fast experience while preserving fast builds and the benefits of Static Generation.

fallback: true will not update generated pages, for that take a look at Incremental Static Regeneration.

Incremental Static Regeneration

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

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every second
    revalidate: 1, // In seconds
  }
}

export default Blog

Reading files within getStaticProps

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

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

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

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

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

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

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

export default Blog

API Routes

API Routes let you create an API endpoint inside a Next.js app. An endpoint that shall be available at localhost:3000/api/hello must have a file pages/api/hello.js with the following content:

// req = HTTP incoming message, res = HTTP server response
export default function handler(req, res) {
  res.status(200).json({ text: 'Hello' });
  // res.json(json) - Sends a JSON response.
  // res.send(body) - Sends the HTTP response
  // res.redirect([status,] path) - Redirects to a specified path or URL
}

To handle different HTTP methods:

export default function handler(req, res) {
  if (req.method === 'POST') {
    // Process a POST request
  } else {
    // Handle any other HTTP method
  }
}

Dynamic API routes

API routes support dynamic routes, and follow the same file naming rules used for pages. For example, the API route pages/api/post/[pid].js has the following code:

export default function handler(req, res) {
  const { pid } = req.query
  res.end(`Post: ${pid}`)
}

Catch-all API routes

pages/api/post/[...slug].js matches /api/post/a, but also /api/post/a/b, /api/post/a/b/c and so on, but it does not match /api/post. For that, you would either need pages/api/post/[[...slug]].js (“optional catch all”), /api/posts.js or /api/posts/index.js.

Do’s and dont’s

You should not fetch an API Route from getStaticProps or getStaticPaths. Instead, write your server-side code directly in getStaticProps or getStaticPaths (or call a helper function).

A good use case for API Routes is handling form input. For example, you can create a form on your page and have it send a POST request to your API Route. You can then write code to directly save it to your database. The API Route code will not be part of your client bundle, so you can safely write server-side code. API Routes can be dynamic, just like regular pages.

API Middleware

Use them to read and/or further modify incoming requests, such as req.cookies, req.query or req.body.

Every API route can export a config object to change the default configs:

export const config = {
  api: {
    bodyParser: {
      sizeLimit: '1mb',
    },
  },
}

bodyParser Enables body parsing, you can disable it if you want to consume it as a Stream:

export const config = {
  api: {
    bodyParser: false,
  },
}

Enabling CORS

npm i cors
# or
yarn add cors
import Cors from 'cors'

// Initializing the cors middleware
const cors = Cors({
  methods: ['GET', 'HEAD'],
})

// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
function runMiddleware(req, res, fn) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result)
      }

      return resolve(result)
    })
  })
}

async function handler(req, res) {
  // Run the middleware
  await runMiddleware(req, res, cors)

  // Rest of the API logic
  res.json({ message: 'Hello Everyone!' })
}

export default handler

TypeScript and API Routes

You can read how to extend req and res objects with TypeScript on the offical doc page.

Deploying your app

next build builds the production application in the .next folder. After building, next start starts a Node.js server that supports hybrid pages, serving both statically generated and server-side rendered pages.

According to the makers of Next.js, the easiest way to deploy Next.js to production is to use the Vercel platform developed by the creators of Next.js. Vercel is an all-in-one platform with Global CDN supporting static & JAMstack deployment and Serverless Functions.

Once you’re signed up, import your application from your Code Repo on Vercel. When it’s done, you’ll get deployment URLs. Click on one of the URLs and you should see the Next.js starter page live.

When you deploy your Next.js app to Vercel, the following happens by default:

Vercel has many more features, such as Custom Domains, Environment Variables and Automatic HTTPS that auto-renew themselves.

Develop, Preview, Ship

We’ve just gone through the workflow we call DPS: Develop, Preview, and Ship.

  • Develop: We’ve written code in Next.js and used the Next.js development server running to take advantage of its hot reloading feature.
  • Preview: We’ve pushed changes to a branch on GitHub, and Vercel created a preview deployment that’s available via a URL. We can share this preview URL with others for feedback. In addition to doing code reviews, you can do deployment previews.
  • Ship: We’ve merged the pull request to main to ship to production.

We strongly recommend using this workflow when developing Next.js apps — it will help you iterate on your app faster.

Exporting and deploying application as static files

You can export your app by generating static files with next build && next export to an out directory. Use -o to specify a custom out directory. The upload those static files on a webserver.

To only export specific pages:

// next.config.js
module.exports = {
    exportPathMap: async function (
        defaultPathMap,
        { dev, dir, outDir, distDir, buildId }
    ) {
        return {
            '/': { page: '/' },
            '/about': { page: '/about' }, // /pages/about.js
            '/p/hello-nextjs': { page: '/post', query: { title: 'hello-nextjs' } }, // /pages/post.js
            '/p/learn-nextjs': { page: '/post', query: { title: 'learn-nextjs' } },
            '/p/deploy-nextjs': { page: '/post', query: { title: 'deploy-nextjs' } },
        }
    },
}

Keep in mind that next/image does not work with static file exporting. You will get an error like this:

Error: Image Optimization using Next.js' default loader is not compatible with `next export`.

Explanation can be read here.

Using NextJS with TypeScript

Create an empty tsconfig.json file in the root of your project and add dependencies:

# If you’re using npm
npm install --save-dev typescript @types/react @types/node

# If you’re using Yarn
yarn add --dev typescript @types/react @types/node

Now run yarn dev to start the server and Next.js will:

  • Populate the tsconfig.json file for you. You may customize this file.
  • Create the next-env.d.ts file, which ensures Next.js types are picked up by the TypeScript compiler. You should not touch this file.

Here is how you use the types:

import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next'

export const getStaticProps: GetStaticProps = async context => {
  // ...
}

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

export const getServerSideProps: GetServerSideProps = async context => {
  // ...
}

This is how you use API Routes with TypeScript:

import { NextApiRequest, NextApiResponse } from 'next'

export default (req: NextApiRequest, res: NextApiResponse) => {
  // ...
}

You can convert pages/_app.js into pages/_app.tsx and use the built-in type AppProps, like so:

import { AppProps } from 'next/app'

function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default App

Environment Variables

Next.js has built-in support for loading environment variables from .env.local into process.env.

// .env.local
DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword

// you can use variables
HOSTNAME=localhost
PORT=8080
HOST=http://$HOSTNAME:$PORT
// 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,
  })
  // You cannot use destructuring, it will fail:
  // const { NEXT_PUBLIC_PUBLISHABLE_KEY } = process.env.
  // ...
}

By default all environment variables loaded through .env.local are only available in the Node.js environment, meaning they won’t be exposed to the browser. In order to expose a variable to the browser you have to prefix the variable with NEXT_PUBLIC_

// pages/index.js
import setupAnalyticsService from '../lib/my-analytics-service'

// NEXT_PUBLIC_ANALYTICS_ID can be used here as it's prefixed by NEXT_PUBLIC_
setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID)

function HomePage() {
  return <h1>Hello World</h1>
}

export default HomePage

Note: .env, .env.development, and .env.production files should be included in your repository as they define defaults. .env*.local should be added to .gitignore, as those files are intended to be ignored. .env.local is where secrets can be stored.

Customizing App Component

Next.js uses the App component to initialize pages. You can override it creating a ./pages/_app.js file if, for example. you want to do some of the following things:

  • Persisting layout between page changes
  • Keeping state when navigating pages
  • Custom error handling using componentDidCatch
  • Inject additional data into pages
  • Add global CSS
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

The Component prop is the active page, so whenever you navigate between routes, Component will change to the new page.

Customizing Document

You can create a ./pages/_document.js to augment your application’s <html> and <body> tags.

import Document, { Html, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

<Html>, <Head />, <Main /> and <NextScript /> are required for the page to be properly rendered. The <Head /> component used here is not the same one from next/head. To set individual <title> tags, you should use next/head in your pages or components.

Document is only rendered in the server, event handlers like onClick won’t work.

Custom Error pages

Next.js provides a static 404 page by default without having to add any additional files. To create a custom 404 page you can create a pages/404.js file. This file is statically generated at build time.

// pages/404.js
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>
}

Same goes for 500 error page

// pages/500.js
export default function Custom500() {
  return <h1>500 - Server-side error occurred</h1>
}

500 errors are handled both client-side and server-side by the Error component. If you wish to override it, define the file pages/_error.js and add the following code:

function Error({ statusCode }) {
  return (
    <p>
      {statusCode
        ? `An error ${statusCode} occurred on server`
        : 'An error occurred on client'}
    </p>
  )
}

Error.getInitialProps = ({ res, err }) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404
  return { statusCode }
}

export default Error

Next CLI

// Start the application in development mode (hot-code reloading, error reporting, etc)
next dev
next dev mydir -p 8080

// build application in current directory's .next folder
next build
next build mydir 

// start in production mode (must run 'next build' before)
next start
next start mydir -p 8080

// Export the application for production deployment
next export

next/router

Introducing the useRouter hook:

import { useRouter } from 'next/router'

function ActiveLink({ children, href }) {
  const router = useRouter()
  const style = {
    marginRight: 10,
    color: router.asPath === href ? 'red' : 'black',
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink

The router object returned by useRouter:

// Current route. That is the path of the page in /pages
pathname: String

// The query string parsed to an object. It will be an empty object during prerendering if the page doesn't have data fetching requirements. Defaults to {}
query: Object

// The path (including the query) shown in the browser without the configured basePath or locale.
asPath: String

// Whether the current page is in fallback mode.
isFallback: boolean

// The active basePath (if enabled).
basePath: String

// The active locale (if enabled)
locale: String

// All supported locales (if enabled).
locales: String[]

// The current default locale (if enabled)
defaultLocale: String

// Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
isReady: boolean

// Whether the application is currently in preview mode.
isPreview: boolean

Router methods

Push

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// Here you would fetch and return the user
const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    if (!(user || loading)) {
      router.push('/login')
    }
  }, [user, loading])

  return <p>Redirecting...</p>
}

Replace

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return <button onClick={() => router.replace('/home')}>Click me</button>
}

beforePopState

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
      // I only want to allow these two routes!
      if (as !== '/' && as !== '/other') {
        // Have SSR render bad routes as a 404.
        window.location.href = as
        return false
      }

      return true
    })
  }, [])

  return <p>Welcome to the page</p>
}

There is also router.back(), router.reload().

You can listen to routing events:

routeChangeStart(url, { shallow }) - Fires when a route starts to change
routeChangeComplete(url, { shallow }) - Fires when a route changed completely
routeChangeError(err, url, { shallow }) - Fires when there's an error when changing routes, or a route load is cancelled
 - err.cancelled - Indicates if the navigation was cancelled
beforeHistoryChange(url, { shallow }) - Fires just before changing the browser's history
hashChangeStart(url, { shallow }) - Fires when the hash will change but not the page
hashChangeComplete(url, { shallow }) - Fires when the hash has changed but not the page

Configuring Testing with Jest in NextJS for TypeScript

npm install typescript @types/react @types/node -D
npm i jest @testing-library/react @types/jest babel-jest @testing-library/jest-dom @testing-library/user-event @testing-library/dom -D

Add to package.json:

"test": "jest --watch"

Add to .eslintrc.json:

{
    "env": {
        "jest": true
    }
}

Add to .babelrc:

{
  "presets": ["next/babel"]
}

Create jest.config.js

// jest.config.js
module.exports = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
};

Create jest.setup.ts

// jest.setup.ts
import '@testing-library/jest-dom';

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.