Using the pages router - vonschappler/Ultimate-React GitHub Wiki

Creating a new project with the pages router

To create a new Next.js project using the "old" pages router, we proceed on the same way as we did to start the Wild Oasis project, just selecting "NO" when the executed script asks if we wish to use the App router.

From this point, we need to note a few things:

  1. The _folder convention does not work when using the pages router
  2. Instead of /app, we have /pages
  3. Any folder like for example, components, needs to be created at the root of the project
  4. The old pages model is not compatible with RSC, meaning that no components are defined as server components - all the components will be rendered on the server once and then be hydrated when sent to the client.
  5. In pages router Next.js will determine the rendering strategy for a page based in the way we fetch data

Routes, pages and navigation

In order to create a new route using the pages router, all that is required is to create a new file inside the pages folder with the name of the route. So a file named test.js would create the route host/test.

Navigation between the pages/routes will work on the save way as we do in the app router, by making ise of the Link component provided by Next.js

Folders created inside the pages folder are responsible for generating nested routes. Those folders must contain at least the index.js file or other pages files, given their own names.

Creating dynamic routes

For creating dynamic routes, we follow a similar convention as used in the app router, with the main difference of this convention, is that the [] is used on the file name that we wish to create the dynamic route.

So in case we wish the route host/users/:userid, we then should create a files named [userid].js inside the users folder, located inside our pages folder.

In order to get the userid param, we make use of a custom hook provided by Next.js, useRouter(), like in the snippet of code below:

import { useRouter } from 'next/router';

export default function User() {
  const router = useRouter();
  return <div>User {router.query.userid}</div>;
}

Creating Layouts with _app.js and _document.js

Unlike the app router with all conventions, the pages router provies us two files, which can be used to create our application layout.

  1. _app.js - this file is used to "initialze" our application.

    This is the central place for importing fonts and defining the root layout of the application. By default this file exports an App function, which accpepts two parameters (Component and pageProps), which are respondible for rendering the content of a page, using the <Component {...pageProps} /> as the children content of the main layout.

  2. _document.js - provides the skeleton of the website, by making use of the elements/components provided by Next.js (HTML, Head, Main and NextScript)

    This file is almost never touched, so we can safely delete it, because Next.js will make use of a skeleton internally.

For the pages model, the only way to create nested layouts is by using react composition patterns, because the pages model does not offer natively a way of creating nested layouts.


Defining Page titles and FavIcon

To define metadata on a website using the pages router, we need to make use of Head component provided by Next.js, returning it in a React fragment alongside the page layout created on the _app.js file, just like shown in the code below:

import Head from 'next/Head';

export default function App({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>Page title</title>
        <link rel='icon' href='/path/to/favicon.png' />
      </Head>
      <Component {...pageProps} />
    </>
  );
}

For setting different titles for different pages, the ideia is pretty similar, using the same component, but this time inside the named page file.


Fetching data on pages router

While working with the app router, we could easily fetch data directly inside a server component, just by marking that as an async function and awaiting for the result of the an API call.

When it comes to data fetching in the pages router, this can be done by using Next.js APIs because now we don't have server components but we still wish to get this data on the server.

getStaticProps API (static rendering)

As discussed above, when the poages router was introduced, React still didn't have created the RSC architecture. This means it was not possible to make use of the features we used so far, like for example data fetching on server components.

This is where one of the Next.js APIs for data fetching, getStaticProps enters. The way in which we use this API is by simply exporting an async function named getStaticProps inside a component which needs data from an external API (for example), while returning an object with the data fetched as the result of this function.

By doing that, the component itselt will have access to that result of the getStaticProps as (the name suggests) a prop just like displayed below:

export async function getStaticProps() {
  const users = await someFunctionToFetchUsers();
  return { props: { users } };
}

export default function Users({ users }) {
  // this just logs users, showing that this component
  // has access to the returned value users so we can
  // so what we want if with on our code
  console.log(users);

  return <div>{/* logic to render users data here*/}</div>;
}

getServerSideProps API (dynamic render)

Usually we make use of the getServerSideProps when we need to fetch data inside components that are dynamically rendenred on the application, like for example a single user page which will display an user data based on their id (/users/:userid) but we can define this manually for non-dynamic routes just by using this Next.js API.

The way we can use the getServerSideProps is shown in the snippet below:

// note that this API has access to the req.query
export async function getServerSideProps({ params }) {
  const user = await someFunctionToFetchUser(params.userid);
  return { props: { user } };
}

export default function Users({ user }) {
  // this just logs users, showing that this component
  // has access to the returned value users so we can
  // so what we want if with on our code
  console.log(user);

  return <div>{/* logic to render user data here*/}</div>;
}

In case we want to make dynamic routes to be rendered as static routes, we can make use of getStaticPaths Next.js API and then convert the getServerSideProps into the getStaticProps.

In case we need some previously data to be revalidated, all we need to do is to return alongside with the fetched data a prop named revalidate, with it's value defined in second. This works on getStaticProps and the following snippet shows how to implement the data revalidation:

export async function getStaticProps() {
  const users = await someFunctionToFetchUsers();
  return {
    props: { users },
    revalidate: 10,
  };
}

Data Mutation: using api routes

API routes is more frequenlty used in the pages router because they are the ONLY way we can mutate data. To create api routes in the pages router we need to:

  • Create a folder named api inside the pages folder
  • Any file created inside the api folder will then be treated by Next.js as an API endpoint, which will be defined by the file name
  • Because we don't have access to server actions in the pages router, we need to create one api endpoint (file inside the api folder) for each data mutation we wish to perform
  • Each API endpont needs to export A SINGLE FUNCTION which has access to two parameters: req and res

The snippet below displays the barebones of an API route created using the Next.js pages router, which will sent a json to the cliet on a get request to that api router.

export default async function handler(req, res) {
  res.status(200).json({ data: 'Test' });
}

Using api routes in components

For this study case, we'll make use for an api route which will handle form sumission. Keep in mind that because this an example, reading the documentation may be necessary in order to implement other features on your applications.

To begin with, let's assume that we are using the api route described by the code below, where submit contact is just a function created to insert data (using any library of your coiche) inside a contact table:

import { submitContact } from 'path/to/submitContact/';

export default async function handler(req, res) {
  if (req.method !== 'POST')
    return res.status(405).json({
      success: false,
      message: 'Please make a POST request',
    });

  const contactData = {};
  const { error } = await submitContact(contactData);

  if (error) {
    console.log(error);
    res.status(500).json({
      success: false,
      message: 'Could not send your message, please try again',
    });
  }
  res.status(200).json({
    success: true,
    message: 'Thanks for your message! We will be in touch soon!',
  });
}

Let's also assume that our contact form is defined as:

import { useState } from 'react';

export default function Contact() {
  function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    const contactData = Object.fromEntries(formData);
  }

  return (
    <>
      <div>
        <h1>Contact Form</h1>

        <form onSubmit={handleSubmit}>
          <div>
            <label>Full name</label>
            <input required name='fullName' />
          </div>

          <div>
            <label>Email address</label>
            <input type='email' name='email' required />
          </div>

          <div>
            <label>Subject</label>
            <input name='subject' required />
          </div>

          <div>
            <label>Message</label>
            <textarea name='message' rows={3} required />
          </div>

          <div>
            <button>Send message</button>
          </div>
        </form>
      </div>
    </>
  );
}

The way we sould then process this form sumition, since it's mutating our data by adding a new entry to our contact table is by making a few changes on the code, like shown below:

// api file:
import { submitContact } from 'path/to/submitContact/';

export default async function handler(req, res) {
  if (req.method !== 'POST')
    return res.status(405).json({
      success: false,
      message: 'Please make a POST request',
    });

  const contactData = JSON.parse(req.body);
  const { error } = await submitContact(contactData);

  if (error) {
    console.log(error);
    res.status(500).json({
      success: false,
      message: 'Could not send your message, please try again',
    });
  }
  res.status(200).json({
    success: true,
    message: 'Thanks for your message! We will be in touch soon!',
  });
}
// form file:
import { useState } from 'react';

export default function Contact() {
  async function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    const contactData = Object.fromEntries(formData);
    await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify(contactData),
    });
  }

  return (
    <>
      <div>
        <h1>Contact Form</h1>
        <form onSubmit={handleSubmit}>
          <div>
            <label>Full name</label>
            <input required name='fullName' />
          </div>
          <div>
            <label>Email address</label>
            <input type='email' name='email' required />
          </div>
          <div>
            <label>Subject</label>
            <input name='subject' required />
          </div>
          <div>
            <label>Message</label>
            <textarea name='message' rows={3} required />
          </div>
          <div>
            <button>Send message</button>
          </div>
        </form>
      </div>
    </>
  );
}

Remember that more complet logic can be created in this scenario, so we could provide any sort of visual input in the UI, so the users can know their current status on the form submission, for example.

⚠️ **GitHub.com Fallback** ⚠️