28 Advanced NextJS features everyone should know

28 Advanced NextJS features everyone should know

Posted 2023-12-31

This is a guide about some lesser-known features of NextJS.

I've included things in this article that I have not seen in NextJS applications that I've worked on. Maybe these features are more popular than I am aware, but anyway, hopefully you come across some features that are new to you. Leave a comment if you have suggestions.

This article assumes you are familiar with NextJS (pages & app router) and I won't go into any basic/typical features here

You can prevent typos in your <Link href="/some-route"> components by turning on the typedRoutes config option.

This will generate a .d.ts types file in .next/types, which has typing information for all of your routes so that Typescript can check the href values are valid.

✅ copied
const nextConfig = { experimental: { typedRoutes: true, }, } module.exports = nextConfig

There are some caveats - the most important being that non literal strings (e.g. dynamically created strings like /blog/${slug} will not work and you must cast it as Route).

See their docs for full info)

Official NextJS packages for some common Google third-party scripts

There are a few official packages for using third-party scripts from Google.

All of the official third-party scripts are from Google - and they can be installed by adding the @next/third-parties/google dependency.

  • GoogleTagManager to instantiate a Google Tag Manager container to your page
  • GoogleAnalytics set up Google Analytics (in their docs, but not on npm package yet)
  • GoogleMapsEmbed to lazy load a Google Maps instance
  • YouTubeEmbed to quickly load a Youtube embed. It uses the lite-youtube-embed package.

Example usage:

✅ copied
import { YouTubeEmbed } from '@next/third-parties/google' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> <YouTubeEmbed videoid="ogfYd705cRs" height={400} params="controls=0" /> </html> ) }

NextJS's recommendation for putting .env in your repository

I'm surprised at this recommendation, although I can understand their reasoning. I have never seen .env committed to git. Sometimes .env.example (dummy values, that you copy over to .env) or .env.test (for CI or to run tests locally).

This is a direct quote from their docs:

Good to know: .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.

Generate a sitemap.xml easily

If you put a sitemap.ts file in your app directory, you can use it to generate an array of files to include in a sitemap.xml file.

The example below shows hard coded sitemap items, but you can turn this function into an async one and dynamically load from an API or database.

✅ copied
import { MetadataRoute } from 'next' export default function sitemap(): MetadataRoute.Sitemap { return [ { url: 'https://acme.com', lastModified: new Date(), changeFrequency: 'yearly', priority: 1, }, { url: 'https://acme.com/blog', lastModified: new Date(), changeFrequency: 'weekly', priority: 0.5, }, ] }

To find out more check out their docs.

unstable_cache (app router)

You can use unstable_cache to cache things, such as database data and use that cached data between multiple queries.

As the name implies, this is unstable. But from my experience, it works as you would expect it to. There is a chance that future versions of NextJS will change how it works though (as well as the name!)

✅ copied
import { getUser } from './data'; import { unstable_cache } from 'next/cache'; const getCachedUser = unstable_cache( async (id) => getUser(id), ['my-app-user'] ); export default async function Component({ userID }) { const user = await getCachedUser(userID); ... }

Improve you CI's build cache by configuring it to use .next/cache

Next will use the .next/cache directory to save some cached data which can be used between different builds.

To use this cache in your CI provider, you normally have to manually configure it.

This is done automatically with Vercel BTW

Here is an example for Github Actions, but there are more examples on the docs with other providers such as CircleCI, GitLab CI, BitBucket Pipelines etc.

✅ copied
uses: actions/cache@v3 with: path: | ~/.npm ${{ github.workspace }}/.next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

Check out the CI docs here for full information.

Get the geo location of a user (if you use Vercel)

Next will use the NextRequest object when passing around request objects. You will probably be familiat with it if you have used route handlers:

export async function GET(request) { // request param is the NextRequest return Response.json('some-response') }

On Vercel you automatically get the geolocation added to the request.

request.geo.city request.geo.country request.geo.region request.geo.latitude request.geo.longitude

If you are deploying with Vercel, you can access the IP with request.ip, or request.headers.get('X-Forwarded-For') on most other servers.

Easily remove JSX properties (such as data-test)

You can easily remove JSX properties with babel-plugin-react-remove-properties. You will probably want to do this with properties such as data-testid="SomeTestId".

It is easy to set up, if you are using Babel.

✅ copied
module.exports = { compiler: { reactRemoveProperties: true, // by default it will remove data-testid // but you can set `properties` to define a regex }, }

Next's dynamic loader

NextJS has next/dynamic, which is a combination of both React.lazy() (lets you defer loading component’s code until it is rendered for the first time) and <Suspense> (lets you display a fallback until its children have finished loading).

Here is an example of how to use it:

✅ copied
'use client' import { useState } from 'react' import dynamic from 'next/dynamic' // Client Components: const ComponentA = dynamic(() => import('../components/A')) const ComponentB = dynamic(() => import('../components/B')) const ComponentC = dynamic(() => import('../components/C'), { ssr: false }) export default function ClientComponentExample() { const [showMore, setShowMore] = useState(false) return ( <div> {/* Load immediately, but in a separate client bundle */} <ComponentA /> {/* Load on demand, only when/if the condition is met */} {showMore && <ComponentB />} <button onClick={() => setShowMore(!showMore)}>Toggle</button> {/* Load only on the client side */} <ComponentC /> </div> ) }

A related feature is how you can also load other scripts/files on demand. Here is an example:

✅ copied
export default function Page() { return ( <button onClick={async () => { const Fuse = (await import('fuse.js')).default const fuse = new Fuse(someData) console.log(fuse.search(value)) }} /> ) }

Understand how the fast refresh (hot reloading) works

Fast refresh is the nearly instant reloading of your components (without losing state) that happens when you edit a file.

The docs cover it in much more detail, but I want to point out a few important things to know about the fast refresh.

  • "If you edit a file that only exports React component(s), Fast Refresh will update the code only for that file, and re-render your component. You can edit anything in that file, including styles, rendering logic, event handlers, or effects."

  • "If you edit a file with exports that aren't React components, Fast Refresh will re-run both that file, and the other files importing it. So if both Button.js and Modal.js import theme.js, editing theme.js will update both components."

  • "Finally, if you edit a file that's imported by files outside of the React tree, Fast Refresh will fall back to doing a full reload. You might have a file which renders a React component but also exports a value that is imported by a non-React component. For example, maybe your component also exports a constant, and a non-React utility file imports it. In that case, consider migrating the constant to a separate file and importing it into both files. This will re-enable Fast Refresh to work. Other cases can usually be solved in a similar way."

Also sometimes the fast refresh might not be desired. For that, you can set // @refresh reset in the file you are working on to force a full reload.


If you have a file called instrumention.ts in the root directory (or in src), it will run an exported function called register when the NextJS server is initially bootstrapped.

You can use this to set up/run external code that must be initialized for your app to run.

Here is an example:

import { init } from 'package-init' export function register() { init() }

Type check your next.config.js file

You must have the file extension of .js for your next.config.js file, so you can't get the full Typescript type checks. However, if you add the following line then your IDE will probably run some type checks:

✅ copied
// @ts-check /** * @type {import('next').NextConfig} **/ const nextConfig = { /* config options here */ } module.exports = nextConfig

Middleware's waitUntil and NextFetchEvent (app router)

If you are using middleware and want to run some code in the background, then you can use waitUntil. Here is an example:

✅ copied
import { NextResponse } from 'next/server' import type { NextFetchEvent, NextRequest } from 'next/server' export function middleware(req: NextRequest, event: NextFetchEvent) { event.waitUntil( fetch('https://my-analytics-platform.com', { method: 'POST', body: JSON.stringify({ pathname: req.nextUrl.pathname }), }) ) return NextResponse.next() }

Multi line and referencing other variables in your .env file

For local dev environments, it is quick and easy to use your .env to set some variables. But occasionally you will need multi-line values. This is possible with your .env file, like this:

✅ copied

You can also reference other variables like this:

TWITTER_USER=nextjs TWITTER_URL=https://twitter.com/$TWITTER_USER

In above example, TWITTER_URL will be set to "https://twitter.com/nextjs"

Route segment control

If you are using the App router, then exporting some values from your page.tsx, layout.tsx or route.ts can configure that route.

This is probably quite well known if you have used the App router, but I thought it was interesting to see all of the exported values you can configure.

✅ copied
export const dynamic = 'auto' export const dynamicParams = true export const revalidate = false export const fetchCache = 'auto' export const runtime = 'nodejs' export const preferredRegion = 'auto' export const maxDuration = 5 export default function MyComponent() {}

BTW these values must be static - they are extracted before runtime. So, export const maxDuration = 5 will work, but export const maxDuration = 2.5 * 2 will not.

For documentation for each config, I suggest reading this.

Use generateViewport on server components to set some special meta tags (app router)

You might want to set some meta such as:

  • <meta name="theme-color" content="#4285f4" />
  • <meta name="color-scheme" content="dark light" />

You can easily do it on server components with generateViewport().

✅ copied
export function generateViewport({ params }) { return { themeColor: 'black', width: 'device-width', initialScale: 1, maximumScale: 1, colorScheme: 'dark', } /* <meta name="theme-color" content="black" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <meta name="color-scheme" content="dark" /> */ }

Use isBot to check if a request is coming from a known bot, and more

If you import userAgent from next/server, it can be used to tell you about the user agent of the incoming request. Some interesting things you can do with this:

  • the isBot boolean property is returned will tell you if it is a known crawler/bot
  • the device boolean to find out about the device (e.g. mobile phone, desktop) the user agent is running on. It includes properties such as model (model of device), type (console/mobile/tablet/etc), and vendor.
✅ copied
import { NextRequest, NextResponse, userAgent } from 'next/server' export function middleware(request: NextRequest) { const { isBot } = userAgent(request) if(isBot) { // do something for these bot users } return NextResponse.rewrite(request.nextUrl) }

next.config.js phase

In your next.config.js file, you might sometimes want to return different configurations for different contexts (build/server/dev/testing.

This shows how you can use it:

✅ copied
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants') module.exports = (phase, { defaultConfig }) => { /* possible values: PHASE_EXPORT = 'phase-export' PHASE_PRODUCTION_BUILD = 'phase-production-build' PHASE_PRODUCTION_SERVER = 'phase-production-server' PHASE_DEVELOPMENT_SERVER = 'phase-development-server' PHASE_TEST = 'phase-test' */ if (phase === PHASE_DEVELOPMENT_SERVER) { return { /* development only config options here */ } } return { /* config options for all phases except development here */ } }

Get more debug info when building

If you run next build --debug from your command line it will build in debug mode.

You can also run next build --profile to run in profile mode.

next info will print out local platform information, such as the operating system and architecture.

Global error page (app router)

Most of us know about layout.ts, and of course page.tsx. There are a few other special filenames, including error.ts and global-error.ts.

  • The root app/error.ts boundary does not catch errors thrown in the root app/layout.js or app/template.js component.
  • But you can use app/global-error.ts to catch these. It wraps the entire application.
  • Because it wraps the entire app - including layout files - you have to include <html> and <body> in global-error.ts.

Read more here (And here for error handling for the Pages Router).

Use the Metadata API to set meta tags (app router)

When using the app router, you should use the Metadata API to set meta tags or title tag (don't just add them to <head> yourself).

✅ copied
import { Metadata } from 'next' // either Static metadata export const metadata: Metadata = { title: '...', } // or Dynamic metadata export async function generateMetadata({ params }) { return { title: '...', } }

See their docs.

Note: a similar file exists for pages router, which is more commonly used - pages/_error.js

<Link ...> component has a few boolean options

We all know the <Link> component, imported from 'next/link'.

This is how you would typically use it:

✅ copied
import Link from 'next/link' export default function YourPage() { return <Link href="/home"> Go home </Link> }

As well as the href property, there are a few boolean props too:

Replace boolean option on NextJS Link component

First is replace. The default is replace={false} so you only need to set it if turning it to true.

When true, next/link will replace the current history state instead of adding a new URL to the browser’s history

✅ copied
import Link from 'next/link' export default function YourPageReplace() { return ( <Link href="/home" replace> Go home (replace) </Link> ) }

You can also do this with useRouter(): router.replace('/home').

Scroll option on the Link component

This is another boolean that defaults to false, so you only need to set it if you are turning it to true.

You have probably noticed when you click a link in NextJS it will scroll to the top of the page (to make it look like a typical <a href> tag on a non-JS site, when it loads the entire page from scratch).

Sometimes you may want to prevent this behaviour and remain at the current scroll position. You can do this with the scroll boolean prop on <Link>:

✅ copied
import Link from 'next/link' export default function Page() { return ( <Link href="/dashboard" scroll> Dashboard (scroll) </Link> ) }

You can also turn off automatic scrolling when using useRouter()'s push() function: router.push('/dash', { scroll: false })

Note you can still use anchors in the URLs, such as this:

<Link href="/dashboard#profile">Profile</Link>


When the prefetch boolean value is turned on (which it is by default), the link's href URL will be prefetched in the background.

This prefetching will happen for links in the current viewport or those that will be visible once you scroll on the page.

✅ copied
import Link from 'next/link' export default function Page() { return ( <> <Link href="/page1" prefetch={false}> Dashboard (will not be prefetched) </Link> <Link href="/page2"> Dashboard (default behaviour - with prefetch enabled) </Link> </> ) }

Prefetching can also be done with useRouter(), such as router.prefetch('/page2')

Configure browserlist to polyfill for older browsers

NextJS will automatically polyfill for recent browsers (and automatically add CSS prefixes where needed with autoprefixer, but you can override specifically what versions it will polyfill for, by adding config to package.json:

✅ copied
{ "browserslist": [ "chrome 64", "edge 79", "firefox 67", "opera 51", "safari 12" ] }

You don't have to run the standard Next server

If you have complex requirements for your application and want to use NextJS, you might benefit from setting up a custom Next server.

So you could for example have an express app that run some routes through standard NextJS rendering logic, but other routes through its own logic.

Here is an example

✅ copied
const { createServer } = require('http') const { parse } = require('url') const next = require('next') const dev = process.env.NODE_ENV !== 'production' const hostname = 'localhost' const port = 3000 // when using middleware `hostname` and `port` must be provided below const app = next({ dev, hostname, port }) const handle = app.getRequestHandler() app.prepare().then(() => { createServer(async (req, res) => { // Be sure to pass `true` as the second argument to `url.parse`. // This tells it to parse the query portion of the URL. const parsedUrl = parse(req.url, true) const { pathname, query } = parsedUrl if (pathname === '/a') { await app.render(req, res, '/a', query) } else { await handle(req, res, parsedUrl) } }) .listen(port, () => { console.log(`> Ready on http://${hostname}:${port}`) }) })

There isn't a great amount of documentation on this, but this stackoverflow explains the getRequestHandler function.

There are some good examples of how it is being used on Github.

Global error page (app router)

Most of us know about layout.ts, and of course page.tsx. There are a few other special filenames, including error.ts and global-error.ts.

  • The root app/error.ts boundary does not catch errors thrown in the root app/layout.js or app/template.js component.
  • But you can use app/global-error.ts to catch these. It wraps the entire application.
  • Because it wraps the entire app - including layout files - you have to include <html> and <body> in global-error.ts.

Read more here (And here for error handling for the Pages Router).

Use the Metadata API to set meta tags (app router)

When using the app router, you should use the Metadata API to set meta tags or title tag (don't just add them to <head> yourself).

✅ copied
import { Metadata } from 'next' // either Static metadata export const metadata: Metadata = { title: '...', } // or Dynamic metadata export async function generateMetadata({ params }) { return { title: '...', } }

See their docs.

Note: a similar file exists for pages router, which is more commonly used - pages/_error.js

Multi zones (multiple Next.JS application deploys)

If you have a large app (or existing apps) you might want to split them up into completely separate apps with their own deployments.

An example would be http://yoursite.com/blog which is it's own blog application, then everything else on that domain as a different application.

You can set this up in your next.config.js in your main app, with rewrites. If you had 2 apps home and blog, with home as the main app:

next.config.js (in main app)
✅ copied
const { BLOG_URL } = process.env // e.g. http://localhost:9999/ module.exports = { async rewrites() { return [ { source: '/:path*', destination: `/:path*`, }, { source: '/blog', destination: `${BLOG_URL}/blog`, }, { source: '/blog/:path*', destination: `${BLOG_URL}/blog/:path*`, }, ] }, }

The next.config.js for the other (blog) application just needs to set a basePath: '/blog.

Then when you run the main app (which has the rewrites), you will run the main app on localhost:9999, but the blog app on localhost:9999. When you visit http://localhost:3000/blog/hi it will be proxied from http://localhost:9999/hi.

So people visiting your site will not know there are two separate apps deployed. This requires a bit more configuration when you serve your site, but if you use Vercel then it is easy to set up.

Check out a demo app here.

Return a 404 or redirect from getServerSideProps() (Pages Router)

This is not exactly a hidden feature or anything, but I've seen some apps show a 404 in the component (and not in calls to getServerSideProps()). It is easy to return a 404 status:

✅ copied
export async function getServerSideProps(context) { const yourData = await getYourData() if (!yourData.isSuccess) { return { notFound: true } } // otherwise continue as normal, // send yourData as props to component: return { props: { yourData } } }

There are a few other special properties you can return too, such as to force a redirect:

pages/somePage.tsx (getServerSideProps)
✅ copied
// ... if(yourData.shouldRedirect) { return { redirect: { destination: '/', permanent: false, }, } } // ...

If you are using getStaticProps then you can also force a 404 or redirect in a similar way. See their docs on getStaticProps here.

Subscribe to my
Full Stack Typescript Developer

If you enjoyed my content and want more Full Stack Typescript content, then enter your email below.

I send a few links every couple of weeks, that I personally found useful from around the web.

Focusing on Typescript, NextJS, React, and also
engineering soft skills posts.

Welcome to my site and blog - Code Driven Development.

This is where I dive topics relating to modern software engineering - with a focus on Typescript, React, web accessibility and Agile practices

I pick topics that I think are interesting, that I think others might find interesting, or write up guides that I wish I had read before learning a topic.