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
Opt in to automatically getting Typescript checks on your <Link>
s
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.
next.config.js✅ copiedconst 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
).
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 pageGoogleAnalytics
set up Google Analytics (in their docs, but not on npm package yet)GoogleMapsEmbed
to lazy load a Google Maps instanceYouTubeEmbed
to quickly load a Youtube embed. It uses the lite-youtube-embed package.
Example usage:
app/layout.tsx✅ copiedimport { 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.
app/sitemap.ts✅ copiedimport { 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!)
someComponent.tsx✅ copiedimport { 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.
build-workflow.yml✅ copieduses: 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.
next.config.js✅ copiedmodule.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:
page.tsx✅ 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:
page.tsx✅ copiedexport 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.
instrumentation.ts
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:
next.config.js✅ 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:
middlware.ts✅ copiedimport { 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:
.env.local✅ copiedPRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- ... Kh9NV... ... -----END DSA PRIVATE KEY-----" PRIVATE_KEY_2="-----BEGIN RSA PRIVATE KEY-----\nKh9NV...\n-----END DSA PRIVATE KEY-----\n"
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.
layout.tsx✅ copiedexport 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().
layout.tsx✅ copiedexport 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 asmodel
(model of device),type
(console/mobile/tablet/etc), andvendor
.
middleware.ts✅ copiedimport { 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:
next.config.js✅ copiedconst { 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>
inglobal-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).
layout.ts✅ copiedimport { Metadata } from 'next' // either Static metadata export const metadata: Metadata = { title: '...', } // or Dynamic metadata export async function generateMetadata({ params }) { return { title: '...', } }
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:
page.tsx✅ copiedimport 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
page.tsx✅ copiedimport 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>
:
page-scroll.tsx✅ copiedimport 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>
Prefetch
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.
page-no-prefetch.tsx✅ copiedimport 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
:
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
server.js✅ copiedconst { 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>
inglobal-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).
layout.ts✅ copiedimport { Metadata } from 'next' // either Static metadata export const metadata: Metadata = { title: '...', } // or Dynamic metadata export async function generateMetadata({ params }) { return { title: '...', } }
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)✅ copiedconst { 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:
pages/somePage.tsx✅ copiedexport 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.