CS 312 Software Development

CS 312 - Practical Routing

Goals

  • Learn how to make multi-page websites with Next.js
  • Learn about dynamic routing
  • Learn to style a site with a Layout component

Background

In class we talked about some of the shortcomings of the Single-Page Application (SPA) approach. One of the primary ones is that we can't bookmark content or share links to content in a meaningful way. The Back button also becomes problematic (think about hitting the Back button when you accidentally went into the editor in Simplepedia, for example).

The React Router library is designed to provide a page-like interface to your React application. It uses the URL as a way to route the user to different component on the site as well as providing mechanisms to traverse between different "pages". So, for example, in Simplepedia, rather than using conditional rendering to navigate between the editor view and the browsing view, we could have used the router to create two virtual "pages" that we navigate between.

Next.js simplifies the use of React Router through its routing mechanism. From our perspective, this is a remarkably simple interface. If we put a file in the pages directory, Next.js will turn it into a routable page. Incidentally, this is why we had to move the tests and CSS files to a different directory.

I encourage you to read through the documentation for creating routes for all of the details, but in this practical I'll attempt to give you enough to go on so that you can use this facility in your projects.

Prerequisites

  1. Visit the Practical Routing page on Replit (or click the 'Start Project' link for Practical Routing on our team page).

  2. Click through to the GitHub classroom assignment to create your private repository.

  3. Return to the assignment on Replit and go to the Version Control panel.

  4. Click the button to create a new git repository (not the one for connecting to an existing repo) OR type git init in the shell.

  5. Commit the current state of the practical to your repository.

  6. Open the shell and follow the instructions on your GitHub repository for connecting an existing repository:

    1. git remote add origin repo-name where repo-name is the name of your repository (e.g., https://github.com/csci312-s21/practical-routing-ChristopherPAndrews.git)
    2. git branch -m main
    3. git push -u origin main

Create a new page

When you open up the starter code, you will find a very simple website.

Let's add a new page to the site. Create a new file in the pages directory called about.js. Give it these contents:

import Head from "next/head"

import styles from "../styles/Home.module.css"

export default function About() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Routing example</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
       <h3>About</h3>
      <p>This practical is all about handling pages and routing in Next.js</p>
      </main>
    </div>
  )
}

If you add /about to the URL in the preview, you will see the new page. Yes, it is just that simple. Next.js will use the name of the file to determine how to route to the component. As is common for web servers, index is treated specially, and is routed as /. You can add folders in here if you like and they will just become part of the route. In essence, you can build a web application the way you might have built a conventional website -- as a directory of separate pages.

*If this all feels similar to what we did in api, there is a reason for that... Just bear in mind that everything in api is executed on the server, while everything else in pages is executed in the client browser.

To navigate between pages, we are going to use a special component called a Link.

Go back to index.js and add an import for the Link component:

import Link from "next/link";

Then, under the message telling you not to panic, add a link to the About page:

  <Link href="/about">
    <a>About</a>
  </Link>

There are a couple of things to point out here:

  • We use the href attribute to tell the Link where to go
  • The URL is relative, we only provide the path relative to the root of the pages directory
  • We need to use the anchor tag (<a></a>) to provide the actual clickable navigation component

Try it out. Navigate to the About page and use the Back button to return. Marvel at all of the technology being deployed to create the illusion that we have a simple multi-page website.

Once you have tried it out, add another Link to the About page called "Home" to return to the main page.

Branding (aka layout components)

One of the refrains I am often repeating is "DRY out your code!" It should strike you that this multi-page approach is the antithesis of DRYing out code. What if my the pages of my site all share a common header, foot and navigation menu? We can make all of those things components, but we are still going to have to duplicate the code that places them on the page. The solution is to create another component that will handle the layout for us.

At this point, we have seen a number of components that were decomposed into smaller components. However, if we wanted to change the children that appeared within the component, we had to use conditional rendering to swap out the contents (think again about Simplepedia and the relationship with the Editor and IndexBar). The parent component may not have known what the child was doing, but it did have to have explicit knowledge of that all of the possible children were.

As it turns out, there is another way. All of the custom components we have created to date have been things that we could insert into the DOM with a single tag (e.g., <WickedCoolComponent />). As it turns out, we can also create components that contain arbitrary elements using paired tags (i.e., the are containers like <div></div>). The elements that are between the start and end tag are passed to the component as a special prop called children. You can then build up the component however you like and insert the children in a appropriate place without ever knowing what components are contained within it. In truth, folks using Material-UI have already been using this feature without realising it from the beginning.

Create a layout component

Create a new file in components called Layout.js.

Give it two props called title and children.

The component should return the following <div>.

    <div className={styles.container}>
      <Head>
        <title>{title}</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div>
        <h1>Routing example</h1>
        <main className={styles.main}>{children}</main>
        <footer>
          <hr />
          CS 312 Routing example
        </footer>
      </div>
    </div>

Notice that this is essentially the same code we had previously for the main page. There are three differences:

  • The title component (which provides the label for the tab) is now configured via the title prop
  • The main body of the page is populated by the children prop
  • I added a <footer> to re-enforce this idea that we are enclosing the children

Return to index.js. Import the Layout and add it to the component. It will replace everything but the contents of the <main> component. Set the title prop to be "Routing example". Note that you don't set children explicitly -- it will be set to the contents of the tag`

Check it out in the browser. When it is working, wrap the About page in the same way.

Dynamic routes

If you look at a URL for something like the Gradescope page for this assignment https://www.gradescope.com/courses/248180/assignments/1213018 (yes, Gradescope uses React), you will see that there are some variables in the URL (which represent the course and the assignment).

How can we handle a URL like that using Next? Well, hopefully the answer is not surprising. Next supports dynamic routes for pages, just like it does for our API routes. We are going to use the same square bracket syntax that we used for our dynamic API routes.

To explore dynamic routing, we are going to bring some data back. If you look in the data directory you will find that I pulled out all of the Doctor Who specials that were in films.json to make a smaller dataset. We are going to display a list of the titles on the main page, and show their details on a dynamic page.

Creating a dynamic route

Create a new page in the pages directory and call it [special_id].js. As with the API routes, the name itself isn't important, however it will be the name we use to access the data inside of our component.

Inside the file, create a new component called Details and give it the following return statement:

 return (
    <Layout title="Dynamic route">
      <main>{show && <ShowDisplay show={show} />}</main>
    </Layout>
  );

To support this, you will need to import the Layout and ShowDisplay components (ShowDisplay is just a simplified version of the components we used in FilmExplorer). Now we just need the show.

To figure out which URL we are looking at, we need to access the router object. To get that, we have a special hook called useRouter. Import the hook with

import { useRouter } from "next/router";

We can then extract the show id from the URL with this code (add it before the return statement in your component)

  const router = useRouter();
  const { special_id } = router.query;

The hook gets the router, and the we extract the value from the router's query property.

Now we have the id, we just need the show. Import the JSON file

import data from "../../data/drwho.json";

and then we can just look the show up

const show = data.find((item) => item.id === +special_id);

Note that we have to convert special_id to an integer because we are pulling it out of the URL string

If you add one of the show ids to the URL string, you should see a detail view with the poster and the overview of the show.

I'll note at this point that the dynamic routes don't have to be numbers, they can be any string you want (though I advise steering clear of special characters and spaces).

Linking to dynamic routes

Now we need to add the list of possible pages to the home page. It should come as no surprise that we are just going to run map over data to build a collection of Link objects.

  const specials = data.map((item) => (
    <li key={item.id}>
      <Link href={`/${item.id}`}>
        <a>{item.title}</a>
      </Link>
    </li>
  ));

You will obviously need to import data for this to work.

Then just insert it into the component under the link to About.

<h3>Specials</h3>
<ul>{specials}</ul>

Try it out -- you should now have working links to the detail views of these specials. Note that this just shows you nothing if the show id is incorrect. A better solution would handle this more gracefully.

Going further

This is really just a brief introduction to what you can do. I encourage you to read the documentation for more of the nitty-gritty details about topics like catch all routes, optional routes, and alternative ways to specify links to dynamic routes.

Finishing Up

  1. Add and commit your changes to Github.
  2. Submit your repository to Gradescope

I will expect that:

  • There is an About page
  • There is a link from the Home page to the About page
  • There is a link from the About page to the Home page
  • There is a dynamic route for looking up Doctor Who details
  • There is a list of links on the Home page linking to the dynamic routes
  • All three pages should be wrapped in a Layout
  • Passes all ESLint checks

Last updated 05/07/2021