My Brain
My Brain

Next.js Overview

This is a general overview based on the course by Max Schwarzmüller on Udemy

What is Next.js?

Next.js is a React framework for production

Key features

  • Server-side (pre-) rendering of pages

    • Prepares the content on the server
    • Prebuilds content. No SEO issues, all content, metadata is there (unlike React)
  • File based routing

    • The structure of the files within the pages folder is the routing

      routing

    • in the example above, routes are automatically created for

      • / ⬅️ rendered by index.js
      • /contact ⬅️ direct child of pages
      • /posts ⬅️ rendered by index.js inside /posts
      • /posts/[slug] ⬅️ filenames wrapped by square brackets route to any identifier
    • Additionally, an api listens in

      • /api/contact
  • Fullstack Capabilities

    • Add backend (server side) code.
    • Create APIs
    • Can store data, get data, authenticate
    • Can be added on top of a React app

How to start

run

npx create-next-app

or

yarn create next-app

Routes

  • Pages created inside the pages folder will become routes.
  • Files thus inside can include normal React code.
  • To create nested routes, create folders inside pages.
  • An index.js file in a nested folder will then be the route of the folder name, so /posts/index.js will be a route on /posts/.
  • Collect-all routes (with an indefinite amount of segments) are created with [...page].js.

Dynamic routes

  • To extract dynamic route data we can use the useRouter hook, so for the route /pages/[itemId]:
import { useRouter } from 'next/router';

export default function DetailPage() {
  const router = useRouter();
  console.log(router.query.itemId) ⬅️ // this returns the value of the url, so if the url is /item2 that would be the console log

  return <h1>Detail Page</h1>
}

useRouter has most of the functions the react router has, such as

const router = useRouter();
router.push('/')
// or
router.replace('/') ⬅️ // which prevents going back to the previous page (useful for after submitting a form)

A dynamic path can be constructed to navigate programmatically like this:

function goToPage() {
  router.push('/${props.pageId}')
}

To navigate and link, import

import Link from 'next/link'

Link expects an href attribute and renders an anchor tag without sending a new request.

getStaticProps

The built-in function getStaticProps() allows us to pre load data, so it runs the function before rendering the component, preloading the data inside of it.

All the code ran inside of it is server-side and never exposed to the frontend, so it can access the file system, use credentials and run server tasks without compromising any information, since nothing ran in it would even be exposed in the frontend.

It is only executed during the building stage.

export async function getStaticProps() {
  // fetch data from an API
  // interact with the filesystem
  // any server-based process!
}

It always returns an object, and passes it to the component with which it is interacting

export default function HomePage({ posts }) { // this props are received from getStaticProps
  return (
    <FeaturedPosts posts={posts} />
  );
}

export function getStaticProps() {
  const featuredPosts = getFeaturedPosts(); // gets my posts

  return { // *always* returns an object
    props: { // most of the times includes a 'props' object, that is returned to the above component.
      posts: featuredPosts,
    },
    revalidate: 1800, // revalidate forces a new request after n seconds since the last one.
    // Keep the number lower for data that needs to be more up-to-date, increase when extra requests aren't necessary.
  };
}

getStaticServerProps

This function triggers a new regeneration for every new request.

Does not run on every build, but on every request.

export default function UserIdPage({ id }) {
  return <h1>{id}</h1>;
}
export async function getServerSideProps(context) {
  const { params } = context;
  const userId = params.uid; ⬅️ // the file containing this functions is [uid].js

  return {
    props: {
      id: `userid-${userId}`,
    },
  };
}

Besides params, the context also receives a request and a response to interact with.

Note that req and res are not present in the context for getStaticProps.

getStaticPaths

This function needs to be used if we are in a dynamic page and we are using getStaticProps.

import path from 'path';
import fs from 'fs/promises'; ⬅️ // Possible because it's used inside getStaticProps!

export default function ProductDetailPage(props) { ⬅️ // Received from getStaticProps
  const { loadedProduct } = props;

  if (!loadedProduct) return <p>Loading...</p>;

  return (
    <>
      <h1>{loadedProduct.title}</h1>
      <p>{loadedProduct.description}</p>
    </>
  );
}

async function getData() {
  const filePath = path.join(process.cwd(), 'data', 'dummy-backend.json');
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  return data;
}

export async function getStaticProps(context) {
  const { params } = context;
  const productId = params.pid;
  const data = await getData();
  const product = data.products.find((prod) => prod.id === productId);

  if (!product) {
    return { notFound: true };
  }

  return {
    props: {
      loadedProduct: product,
    },
  };
}

export async function getStaticPaths() {
  const data = await getData();
  const ids = data.products.map((product) => product.id);
  const pathsWithParams = ids.map((id) => ({ params: { pid: id } }));

  return {
    paths: pathsWithParams, ⬅️ // paths needs an array of all possible paths. In this case generated with the map method.
    fallback: true,
  };
}

getStaticPaths returns a list of dynamic paths for which the page should be regenerated on getStaticProps.

The paths attribut getStaticPaths returns needs to contain an array as such

return {
  paths: [
    {
      params: {
        key: value ⬅️ // these are the values for the pages to be pre generated.
      }
    },
    {
      params: {
        key: value ⬅️ // this typically happens with some constructor, like map().
      }
    }
  ]
}

getStaticPaths needs a fallback attribute, which accepts true, false or "blocking"

fallback: false will render a 404 if the path is not found.

fallback: true will search for the missing page and eventually generate it on demand and afterwards cache it.

Blocking will block page render until data is fetched.

API Routes

Pages in the api folder will be treated as api routes.

Api pages don't return jsx.

We can use credentials and secret data. Api routes only run in the server.

export default function handler(req, res) {
  if (req.method === 'POST') {
    // Only executes on post requests.
    // allows to run server specific cope talking to the api.
    // It's purely backend code
  }
}

Connect to a database

Using MongoDb as example, see the code below:

import { MongoClient } from 'mongodb';

export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { email, name, message } = req.body;

    const newMessage = {
    email,
    name,
    message,
  };

    let client;

    try {
      client = await MongoClient.connect(process.env.DB_URI);
    } catch (error) {
      res.status(500).json({ message: 'Could not connect to database' });
      return;
    }

    const db = client.db();

    try {
      const result = await db.collection('messages').insertOne(newMessage);
      newMessage.id = result.insertedId;
    } catch (error) {
      client.close();
      res.status(500).json({ message: 'Store message failed!' });
      return;
    }

    client.close();

    res
      .status(201)
      .json({ message: 'Message successfully sent!', feedback: newMessage });
  }
}

Sending a request to the API

Conside the following code to send a post request to the previous api:

async function sendContactData(contactDetails) {
  const response = await fetch('/api/contact', {
    method: 'POST',
    body: JSON.stringify(contactDetails),
    headers: {
      'Content-Type': 'application/json',
    },
  });

  const data = await response.json();

  if (!response.ok) throw new Error(data.message || 'Something went wrong!');
}

async function sendMessageHandler(event) {
  event.preventDefault();

  try {
    await sendContactData({
      email: enteredEmail,
      name: enteredName,
      message: enteredMessage,
    });
  } catch (error) {
    // Error handling
  }
}

The Head element

The Head element renders in components (or in the app) and can wrap title, meta tags, etc.

The _document file

The default _document.js file lives in the pages folder.

Its default content is:

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

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    return { ...initialProps }
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

It can be used to add additional properties, such as lang, like this:

  render() {
    return (
      <Html lang="en">
      [...]

Backlinks