Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@npodonnell
Last active February 14, 2022 14:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save npodonnell/1d9668dad4f290218aadd05d92f08615 to your computer and use it in GitHub Desktop.
Save npodonnell/1d9668dad4f290218aadd05d92f08615 to your computer and use it in GitHub Desktop.
Next.js Crash Course

Next.js Crash Course

N. P. O'Donnell, 2021

Based on:

Starting a Next.js project

Ensure you have at least NodeJS 10.13 installed. Node version can be checked with:

node -v

create-next-app is the easiest way to get started:

cd ~/src # Or wherever you store your projects
npx create-next-app --use-npm

create-next-app initalizes git, creates a .gitignore, and makes an initial commit.

Turn off Telemetry:

npx next telemetry disable

Start up local dev server:

cd <app directory>
npm run dev # Alternatively 'next dev'

Production build:

npm run build # Alternatively 'next build'

Run production build:

npm start # Alternatively 'next start'

Navigate to http://localhost:3000. A Next.js welcome page should be visible.

Next.js Examples

A large collection of Next.js example projects can be found here. Any of these projects can be created locally using create-next-app with the commands:

EXAMPLE=<example name> # e.g. data-fetch
npx create-next-app --example $EXAMPLE $EXAMPLE-app

Next.js Project Structure

Next.js uses a configuration-over-code approach, allowing the behaviour of a Next.js app to be inferred from the directory structure and naming conventions.

Files and Directories

The top-level of a Next.js project contains some or all of the following files/directories:

  • package.json - Standard Node.JS package file - contains standard Next.js scripts and dependencies, plus whatever else your project needs.
  • package-lock.json - Standard Node.JS package lock file - Locks versions of dependencies for security and build reproducibility.
  • node_modules - Standard NodeJS directory. Contains all the project's dependencies.
  • public - Static files including images and favicon.ico are placed in here. When the application is run these files are accessible under the web root (/) and not available under /public.
  • pages - React components representing pages go here. Each page can be accessed on the web under /<pagename> e.g. /pages/blog.jsx can be accessed under /blog.
    • pages/api - API endpoints including GraphQL.
  • styles - CSS Stylesheets.
  • components - React Components that are not pages - e.g. layouts and simple components.

Components and Pages

All pages in a Next.js app are React components and live in the /pages directory. All other components live in the /components directory. The convention is to use lower case for the file names in the /pages directory - e.g. index.js, about.js since the file name controls the path - which are typically lower-case. e.g. https://mysite.com/about. The components in the components directory have the same file name as their default exported component and the React convention is to use PascalCase, for example:

// coomponents/FormattedTime.js
export default function FormattedTime(date) {
  return (
    <span>
      {date.getHours()}:{date.getMinutes()}:{date.getSeconds()}
    </span>
  )
}

Stylesheets

The /styles directory contains all stylesheets including globals.css. This stylesheet is used to apply styles globally, and may only be imported in /pages/_app.js. Individual React components can have their own private stylesheet by using the naming convention <component name>.module.css. The stylesheet is then imported into the component using the convention:

import styles from "../styles/<component name>.module.css"

Head

To add a title, meta tags and anything that traditionally goes inside the <head/> of a HTML page, import the Head component from next/head:

import Head from 'next/head'

export default function Home() {
  return (
    <>
      ...
      <Head>
        <title>My web page</title>
        <meta name="keywords" content="spider food"/>
      </Head>
      ...
    </>
  )
}

Meta Tags

Next.js automatically adds some commonly used meta tags to the <head/> section of each page:

<meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width"/>

Links

Links should be added using the <Link/> component which can be imported from next/link:

import Link from "next/link"

...
    <Link href="/">Home</Link>
    <Link href="/about">About</Link>
...

Data Fetching

getStaticProps

One method of hydrating a Next.js page with data is by using getStaticProps. getStaticProps is an async function exported from the page's .js file which performs some data fetching or data generation which are then passed to the pages as props.

It's important to note that getStaticProps is executed at build time so if it's calling an API for example, the result from that API call will be as called from the build server. The result of the build-time call to getStaticProps is then baked into the HTML, meaning when somebody views the page, no computation or fetch needs to be made, and the pre-rendered content is displayed quickly. However the data does not change, and may go stale. For example if the data contains a dynamically generated timestamp, that timestamp will be stuck at the time when the app was last build.

To keep the page from going stale, getStaticProps can return the revalidate property, which causes the page to be regularly re-generated by the Next.js server during run time. This keeps the page up to date, but without requiring an API call for every page view so is a very efficient way of showing cachable content that doesn't change often such as news or weather. It also doesn't add any latency from a potentially slow API.

If the revalidate property is set, the API must be accessible at both build time and run time, on the respective build and run hosts. Typically getStaticProps will get content over the network using fetch or XMLHttpRequest. The props property returned from getStaticProps is then automatically passed into the page component as the first argument.

getStaticProps is passed a single argument called context which contains the following keys:

  • params (Object) - Route parameters for pages which use dynamic routes. Could contain things like a user ID or the slug of a blog post.
  • preview (Boolean) - true if the page is in preview mode, otherwise undefined.
  • previewData (Object) - Preview data when in preview mode.
  • locale (String) - Contains the active locale (if enabled).
  • locales (List<String>) - Contains all supported locales (if enabled).
  • defaultLocale (String) - Contains the configured default locale (if enabled).

getStaticProps returns an object with the following keys:

  • props (Object, Required) - The props themselves.
  • revalidate (Number, Optional) - Amount in seconds after which a page re-generation can occur.
  • notFound (Boolean, Optional) - If true will cause a 404 page to be shown.

More information on getStaticProps can be found here

getServerSideProps

If the function getServerSideProps is defined in a page's file, it will be called on every request, meaning the user will see the most up-to-date data.

getServerSideProps is passed a single argument called context which contains the following keys:

  • params (Object) - Route parameters for pages which use dynamic routes. Could contain things like a user ID or the slug of a blog post.
  • req (Object) - The HTTP IncomingMessage object.
  • res (Object) - The HTTP response object.
  • query (Object) - An object representing the query string.
  • preview (Boolean) - true if the page is in preview mode, otherwise undefined.
  • previewData (Object) - Preview data when in preview mode.
  • resolvedUrl (String) - A normalized version of the request URL that strips the _next/data prefix for client transitions and includes original query values.
  • locale (String) - Contains the active locale (if enabled).
  • locales (List<String>) - Contains all supported locales (if enabled).
  • defaultLocale (String) - Contains the configured default locale (if enabled).

getServerSideProps returns an object with the following keys:

  • props (Object, Required) - The props themselves.
  • notFound (Boolean, Optional) - If true will cause a 404 page to be shown.
  • redirect (Object, Optional) - Allows redirecting to internal and external resources.

More information on getServerSideProps can be found here

Dynamic Routing

Creating a .js file under /pages whose filename includes a token within two square brackets causes that token to be interpreted as a parameter, which can then be read by the next router. For example, if the following file is saved as /pages/[id].js, navigating to http://localhost:3000/123 will cause the id from router.query to take on the value 123:

import {useRouter} from 'next/router'

export default function Id(props) {
    const router = useRouter()
    const {id} = router.query
    return (
        <div>
            Id: {id}
        </div>
    )
}

Output:

Id: 123

Dynamically routed pages are usually scoped to subdirectories, allowing nice URL formats:

URL Page Naming Convention Notes
https://mysite.com/article/why-nextjs-is-awesome /pages/article/[articleSlug].js Allows multiple scoped entities - e.g. user, article, post, etc.
https://mysite.com/blog/2021/02/posts /pages/blog/[year]/[month]/posts.js Allows multiple pages to have the year / month context.
https://mysite.com/users/ElonMusk /pages/users/[userName]/index.js Allows the page to have it's own directory.

API Routes

API routes can be defined in a Next.js app and by convention are stored under /pages/api. Each file (or directory containing an index.js) in /pages/api exports as its default export an ExpressJS-style handler.

function handler(req, res) {
    res.status(200).json({
        message: "Hello, World"
    })
}

export default handler

If the above file is saved as /pages/api/hello.js, the handler may then be accessed from http://localhost:3000/api/hello and will return a 200 HTTP response, with the application/json content type and body:

{ 
    "message": "Hello, World"
}

Dynamic Routing

API routes also support dynamic routing. Parameters may be accessed from req.query:

function handler(req, res) {
    const { id } = req.query
    res.status(200).json({
        id
    })
}

Accessing the API

The API endpoints my be accessed from within the app using fetch, however Next.js requires that absolute URLs are used. Relative URLs are not supported:

// Works.
const hello = await (await fetch("http://localhost:3000/api/hello")).json()

// Doesn't work.
const hello = await (await fetch("/api/hello")).json()

GraphQL

GraphQL is easy to integrate into a Next.js app.

Server

A GraphQL endpoint can be set up within a Next.js app under the /pages/api directory. First install the following dependencies:

npm i graphql apollo-server-micro --save

Next, create a file called /pages/api/graphql.js, or alternatively /pages/api/graphql/index.js. This file's default export will be a handler managed by Apollo server. The following is a minimal setup which defines one GraphQL query: hello:

import { ApolloServer, gql } from "apollo-server-micro"

const typeDefs = gql`
    type Query {
        hello: String!
    }
`

const resolvers = {
    Query: {
        hello(parent, args, context) {
            return "Hello!"
        }
    }
}

const apolloServer = new ApolloServer(
    {
      typeDefs,
      resolvers
    }
)

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

export default apolloServer.createHandler({ path: '/api/graphql' })

To test the server, navigate to http://localhost:3000/api/graphql, and enter the query:

{ hello }

Expected output:

{
  "data": {
    "hello": "Hello!"
  }
}

Client

The Apollo GraphQL client is the preferred way of calling Apollo GraphQL endpoints. First install the client:

npm i @apollo/client --save

Creating the Client

The following code creates an Apollo GraphQL client with in-memory cache, pointed to the local /api/graphql endpoint:

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
    uri: 'http://localhost:3000/api/graphql',
    cache: new InMemoryCache()
});

Save the client somewhere near the top level such as /utils.

The client can then be imported into a page or component and utilized, for example in getServerSideProps:

import {gql} from "@apollo/client"
import gqlClient from "../../utils/gqlClient";

...

async function getServerSideProps() {
    const helloResult = await gqlClient.query({
        query: gql`
            {hello}
        `
    })
    
    const { hello } = helloResult.data
    
    return {
        props: {
            hello
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment