NextJS notes

Published on

React has taken over the front end world, and a popular framework for working with larger SPAs is NextJS. It handles SSR, SSG, routing and much more.

Auth

You can use the next-auth package for auth.

https://nextauth.js.org

It has built in support for many 'providers' (such as login with Google). But you can also use it with your own auth system (storing username/email in your database), from scratch.

router

Next comes with its own built in router.

  • const router = useRouter() to get the router in your components
  • router.push('/new-path') to move to a new url
  • router.replace('/new-path') to move to a new url and prevent ability to go back (useful when logging in/out for example)

fallback in getStaticPaths

In getStaticPaths() you define a list of all urls for a page with a dynamic url.

But maybe you can't build every single page (what happens if you add a a new blog post/content - at build time it won't know about this).

So you can use the fallback option.

  • fallback: true will render the page initially empty, and FE will get new data
  • fallback: blocking will not return anything until BE has the data (props). Slower first byte to end user, but no loading logic needed in components. introduced here
  • fallback: false to show a 404

If you are using fallback: true then you can use code like this to show a loading page:

function YourPage(props) {
    const router = useRouter();
    if(router.isFallback) {
        return <h1>Loading posts...</h1>
    }
    
    return <YourNormalContent post={props.post} />`
}

Custom document file

You can create a pages/_document.js file like this:

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

export default function Document() {
  return (
    <Html>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

if you need to set custom HTML outside of your main app. Useful for setting <html>, <head> and your <body> tag. Note: this is always rendered only on server side, and you cannot put any event handlers like onClick here

You might want to use this when setting things like the language attribute in the html tag: <html lang="fr">, or classes to your body tag.

Within _document.js you have to include <Html>, <Head />, <Main /> and <NextScript /> (all imported from next/document).

Custom error pages

Create a page like pages/404.js for a custom 404 page. You can also create a page such as pages/500.js for a custom page when a 500 error is encountered. getStaticProps() works with these pages.

You can also implement a custom pages/_error.js to handle more logic. There is a prop passed in called statusPage which you can use to determine the type of error.

// pages/_error.js
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

You can also use the default error page in another component, by importing import Error from 'next/error'

import Error from 'next/error'

export async function getServerSideProps() {
  const res = await fetch('https://api.github.com/repos/vercel/next.js')
  const errorCode = res.ok ? false : res.status
  const json = await res.json()

  return {
    props: { errorCode, stars: json.stargazers_count },
  }
}

export default function Page({ errorCode, stars }) {
  if (errorCode) {
    return <Error statusCode={errorCode} />
  }

  return <div>Next stars: {stars}</div>
}

What is Incremental Static Regeneration

Incremental Static Regeneration (or ISR) is a way to create pages (or update existing pages/content) without redeploying the whole app..

You can generate static pages at runtime (not build-time) with ISR. This is useful if you have lots of pages (such as an ecommerce site). With ISR you can define when Next should update (with revalidate prop)

To use ISR in NextJS, add revalidate prop to getStaticProps

Example:

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

// 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 10 seconds
    revalidate: 10, // In seconds
  }
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // We'll pre-render only these paths at build time.
  // { fallback: 'blocking' } will server-render pages
  // on-demand if the path doesn't exist.
  return { paths, fallback: 'blocking' }
}

export default Blog

ISR would not be good for something like a live chat. But useful for more static things like ecommerce items, or maybe a blog.

getInitialProps vs. getServerSideProps

In NextJS 9.3 getServerSideProps was introduced. This replaced getInitialProps in most cases.

There is one main difference to be aware of - when your users click a <Link>.

When your user visits a page, that request will run getInitialProps() on the server, then if the user clicks a <Link> to a new page, the new page's getIniitialProps() will be run on the client side.

But of course getServerSideProps only runs on the server.

I think at this stage, in 2023, getIniitalProps should be considered deprecated and avoided.

Other things to be aware of:

Data returned from getInitialProps is serialized when server rendering, similar to what JSON.stringify does. Make sure the returned object from getInitialProps is a plain Object and not using Date, Map or Set.

For the initial page load, getInitialProps will run on the server only. getInitialProps will then run on the client when navigating to a different route via the next/link component or by using next/router. However, if getInitialProps is used in a custom _app.js, and the page being navigated to implements getServerSideProps, then getInitialProps will run on the server.

getInitialProps can not be used in children components, only in the default export of every page

Forcing hard refresh (instead of fast refresh)

Sometimes you might want to force the state to be reset, and a component to be remounted. For example, this can be handy if you're tweaking an animation that only happens on mount. To do this, you can add // @refresh reset anywhere in the file you're editing. This directive is local to the file, and instructs Fast Refresh to remount components defined in that file on every edit.

Notes on typescript and nextjs

A file named next-env.d.ts will be created at the root of your project. This file ensures Next.js types are picked up by the TypeScript compiler. You should not remove it or edit it as it can change at any time. This file should not be committed and should be ignored by version control (e.g. inside your .gitignore file).

TypeScript strict mode is turned off by default. When you feel comfortable with TypeScript, it's recommended to turn it on in your tsconfig.json.

Instead of editing next-env.d.ts, you can include additional types by adding a new file e.g. additional.d.ts and then referencing it in the include array in your tsconfig.json.

Typings for prop functions

For getStaticProps, getStaticPaths, and getServerSideProps, you can use the GetStaticProps, GetStaticPaths, and GetServerSideProps types respectively:

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

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

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

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

The config files such as next.config.js must be a JS file, but you can enable checks (of your JSDoc comments) with // @ts-check at the top of your JS file:

// @ts-check

/**
 * @type {import('next').NextConfig}
 **/
const nextConfig = {
  /* config options here */
}

module.exports = nextConfig

More typescript typings for nextjs can be found here

Env vars

Note: In order to keep server-only secrets safe, environment variables are evaluated at build time, so only environment variables actually used will be included. This means that process.env is not a standard JavaScript object, so you’re not able to use object destructuring. Environment variables must be referenced as e.g. process.env.PUBLISHABLE_KEY, not const { PUBLISHABLE_KEY } = process.env.

You can also reference other env vars in your .env:

# .env
HOSTNAME=localhost
PORT=8080
HOST=http://$HOSTNAME:$PORT

# If you are trying to use a variable with a $ in the actual value, it needs to be escaped like so: \$.
# .env
A=abc

# becomes "preabc"
WRONG=pre$A

# becomes "pre$A"
CORRECT=pre\$A

If you want to expose env vars to the client (browser), you can easily do that by adding a NEXT_PUBLIC_ prefix to your env vars. .

For example if you had this in your .env: NEXT_PUBLIC_ANALYTICS_ID=your-id-you-need-in-FE-code

Then you can reference it with this:

// pages/footer.js
import YourAnalyticsTool from 'your-analytics-tool'

// 'NEXT_PUBLIC_ANALYTICS_ID' can be used here as it's prefixed by 'NEXT_PUBLIC_'.
// It will be transformed at build time to `<YourAnalyticsTool key={'your-id-you-need-in-FE-code'} />)`.
function Footer() {
  return <div>
            Copyright your-site.com<br />
      <YourAnalyticsTool key={process.env.NEXT_PUBLIC_ANALYTICS_ID} />
  </div>
}

export default HomePage

Any <Link /> in the viewport (initially or through scroll) will be prefetched by default (including the corresponding data) for pages using Static Generation. The corresponding data for server-rendered routes is fetched only when the <Link /> is clicked.

Different safe ways to generate links:

<Link href={`/blog/${encodeURIComponent(post.slug)}`}>
    {post.title}
</Link>

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

When you deploy a new version of your app, and a user clicks a link on the old version, NextJS will turn that <Link> to work like a typical <a> and do a hard refresh

Next.js will automatically load the latest version of your application in the background when routing. For client-side navigations, next/link will temporarily function as a normal <a> tag.

Note: If a new page (with an old version) has already been prefetched by next/link, Next.js will use the old version. Navigating to a page that has not been prefetched (and is not cached at the CDN level) will load the latest version.

Routing

Optional params can be defined with double square brackets.

For example pages/post/[[...slug]].js will match:

  • /post (this is what the optional param does - allows matching with 0 params)
  • /post/foo
  • /post/foo/bar

If you want to link to a page you're on, without hydrating it with the NextJS data fetching methods (getServerSideProps, getStaticProps, and getInitialProps) then you can use the shallow: true option when pushing.

router.push('/?counter=10', undefined, { shallow: true })

So in that example, it would link to the same path but update the query string. The data fetching methods would not change.

NextJS 13

page.tsx

In previous versions of NextJS you could have a pages folder structure like this:

/about/index.tsx // http://localhost/about
/about/contact.tsx // http://localhost/about/contact

But in NextJS you should only use page.tsx, and just nest it in its own subdir, like this:

/about/page.tsx // http://localhost/about
/about/contact/page.tsx // http://localhost/about/contact
  • .js, .jsx, .tsx extensions can be used
  • in NextJS 13, pages are server components by default (but you can make them client components)

Route groups - useful to have different layouts

In your app directory (NextJS 13), you can group routes.

This means you can:

  • Organise routes without changing the URL structure.
  • Use specific route segments in a layout.
  • Create multiple root layouts, over different areas of your app

You create a grouped route by naming a directory with brackets, e.g. (admin) (app/(admin)/account/page.tsx)

This group name does not affect the URL (so the above example would be on http://localhost/account (not http://localhost/admin/account). You can also put custom layout.ts files inside the group directory and it will use that layout instead of any default one.

The actual group name (inside the brackets - 'admin' in my example) has no impact on any URLs, and you can name the groups what ever you like.

Multiple grouped routes should not define the same URL path:

For example, since route groups don't affect URL structure, (marketing)/about/page.js and (shop)/about/page.js would both resolve to /about and cause an error.


(some code samples were from official nextjs docs)