Skip to content

React Router

Learning Objectives

  • Identify what a SPA is, along with the advantages and disadvantages
  • Know what React Router does, and be able to install it
  • Be able to implement a multi-page website design using Router

What is routing?

React is designed to be used for building Single Page Applications, or SPAs. Those single pages host entire applications, meaning the URL (or path) never changes regardless of changes to state withing the application itself. Contrast this with a "traditional" website where following links and interacting with the page modifies the URL. In practice, we sometimes want websites that have different paths for different pages.

An application structured like this has its downsides though. An HTML/JS app would normally store different HTML files on the server (or perhaps dynamically generate them on the server) and requests to different URLS would result in different pages being sent back to the client as responses. But this is not the model for an SPA. React lets us move away from this dependency on the request-response cycle and use JavaScript in the browser to update the DOM as appropriate.

React Router can give us the best of both worlds. Page changes are handled on the client side and any visible content changes are done without fetching a new HTML file. We get the functionality of a multi-page website, with unique URLs for the different sections, but without the time delay of having to constantly send GET requests to the server for various files.

Setting Up React Router

In this example we will build on the full-stack chocolates application from the previous lesson. Instead of the chocolate list and the form to add a new chocolate being together we will show each on a separate page. We will also add navigation links which will be present in both views as well as at the home page.

There are multiple versions of React Router being used in industry. Version 5 persists in a lot of legacy appliactions but we will show you how to implement version 6.4. Visually there is virtually no difference betwene the two, however under the hood there are significant changes to how routes are defined. The documentation for both is still available, and React Router have a page in their documentation that specifically refers to upgrading from v5 to v6 should you ever need it.

React router is available through npm and can be installed in the usual way:

Terminal
# terminal

npm i react-router-dom

Defining Routes

Much like the RESTful routes we wrote for our Spring APIs, we need to consider the format of the routes we define in our React applications. We will wim to follow the RESTful convention as much as possible, although since we will only be making GET requests through the browser we can't rely on using different HTTP verbs to differentiate between our requests any more.

We will have three routes available in our application:

  • localhost:3000 - the default route
  • localhost:3000/chocolates - a list of all chocolates stored in the database
  • localhost:3000/chocolates/new - a form for adding a new chocolate

Unlike with the API, where showing and adding chocolates were both done using the /chocolates route with either a GET or POST request, in this scenario we first need to display a form for the user to add the details of the chocolate to. The convention is to append /new to the resource we are adding. We still make use of the POST request to /chocolates in the form's submit handler.

The react-router package includes some pre-configured components which provide routing functionality. They track history, allow us to define dynamic routes and support navigation between pages using specially-defined links. As a minimum we need to import two things into our app: a function which we use to define our routes and a component in which they will be rendered.

ChocolateContainer.js
// ChocolateContainer.js

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

// ...

The createBrowserRouter function is used to define the routes which will be available in our application. Each route is provided as an object with values specifiying the path we want to make available to the user and the component which should be rendered when the request is made, including any props required. The objects are then passed in an array into the function.

Our application has two components rendered by the container: ChocolateList and ChocolateForm. We define the routes specified above as follows:

ChocolateContainer.js
// ChocolateContainer.js


const chocolateRoutes = createBrowserRouter([
    {
        path: "/chocolates",
        element: <ChocolateList
            chocolates={chocoates}
            deleteChocolate={deleteChocolate}
        />
    },
    {
        path: "/chocolates/new",
        element: <ChocolateForm
            estates={estates}
            postChocolate={postChocolate}
        />
    }
]);

The createBrowserRouter function returns an object which can then be passed as a prop to the RouterProvider component. This replaces our existing component structure in the container's return statement.

ChocolateContainer.js
// ChocolateContainer.js


return(
    <>
        <h1>Single Origin Chocolate</h1>
        <RouterProvider router={chocolateRoutes}
    </>
);

Loading our application doesn't look too promising though - we get a 404 error on localhost:3000! Look carefully at the error though, it isn't like any we've seen previously. Instead we have a message provided by React Router telling us that we haven't specified an element for the requested path. Navigating to localhost:3000 is successfully loading our app and the RouterProvider component but once we are in there we don't have anything telling us what to load at the / route. However, if we visit either localhost:3000/chocolates or /localhost:3000/chocolates/new we see the expected components - our form even submits a chocolate! The user experience is pretty terrible though. Not only do we have to manually enter the routes into the address bar, we also get an error as soon as we open the app.

Adding Navigation

We can address both issues at once by adding a navigation bar to the application. Not only will this add links to each of our routes, it will give us something to display on the home page. Since we want to reuse the links over and over we will define them within their own component.

Terminal
# terminal

touch src/components/Navigation.js

We will use the Link component provided by React Router to handle the links.

Navigation.js
// Navigation.js

import {Link} from 'react-router-dom';

In the past we would have used an a element in HTML to define a link, which in turn could have been placed inside other elements such as list elements. The href attribute specifies the route which the link directs the user to.

<a href="url/goes/here">Text to Display</a>

The syntax for a Link component is similar. Unlike most other components it requires a closing tag and we pass the url as a prop.

<Link to="url/goes/here">Text to Display</Link>

In our app we will place our links in a bullet-point list at the top of our page. Just because we're working in React it doesn't mean we can forget about semantic HTML, so we will add a nav element too.

Navigation.js
// Navigation.js

const Navigation = () => {

    return(
        <>
            <nav>
                <ul>
                    <li><Link to="/chocolates">All Chocolates</Link></li>
                    <li><Link to="/chocolates/new">Add New Chocolate</Link></li>
                </ul>
            </nav>
        </>
    )

}

We want our navigation to show up on the page when we start our app which means we need to update our router structure to include it. We will add it to the start of the list, but we could add it to the end and the app will still work. There is no requirement for a particular ordering from a technical perspective. There is, however, a need for readbility and it can be helpful to have the route for the landing page defined first. Note that this is not the case for all versions of React Router; older versions can use partial matching when searching for a route and so steps need to be taken to ensure we return the correct thing, with ordering being an important tool.

ChocolateContainer.js
// ChocolateContainer.js

const chocolateRoutes = createBrowserRouter([
    {
        path: "/",
        element: <Navigation />
    },
    {
        path: "/chocolates",
        element: <ChocolateList
            chocolates={chocoates}
            deleteChocolate={deleteChocolate}
        />
    },
    {
        path: "/chocolates/new",
        element: <ChocolateForm
            estates={estates}
            postChocolate={postChocolate}
        />
    }
]);

This gives us a solution to one of our problems - we now have navigation on the home page. We don't, however, have navigation on either of the other routes. By default React Router can only display one component per route but that often leads to situations like ours where we can't then render all of the components we need to complete our UI. The solution is to define one or more components as children of another. The path can then be split into two (or more) parts. The first part determines which parent component is rendered with the second determining the child. The child can have its own children in turn, with the components being displayed determined by the full path.

ChocolateContainer.js
// ChocolateContainer.js

const chocolateRoutes = createBrowserRouter([
    {
        path: "/",
        element: <Navigation />,
        children: [ 
            {
                path: "/chocolates",
                element: <ChocolateList
                    chocolates={chocoates}
                    deleteChocolate={deleteChocolate}
                />
            },
            {
                path: "/chocolates/new",
                element: <ChocolateForm
                    estates={estates}
                    postChocolate={postChocolate}
                />
            }
        ]
    }
]);

In our example any route beginning localhost:3000 will render a Navigation component. Additional components are determined by the remainder of the path as before: localhost:3000/chocolates will render ChocolateList, localhost:3000/chocolates/new will render ChocolateForm. If we modify the path property of the parent then the path to each of the children changes as well. For example, if we change the "/" path to "/sweetshop":

  • localhost:3000 will give us the same 404 error we saw earlier
  • localhost:3000/sweetshop will render a Navigation component
  • localhost:3000/sweetshop/chocolates will render the ChocolateList

By constructing the router object in this way we can build some quite complex structures, opening the door to some interesting page designs.

We're not quite done though, as we have one more problem to overcome. Clicking either of the links in our nav bar will redirect the browser to the appropraite page and won't throw any errors, but it doesn't display our components either. Now that Navigation could have children to render we need to tell it where to put them, which we will do by adding an Outlet component. Outlet is another component provided by React Router and acts as a placeholder for any children. When Navigation is rendered with a child the child component will replace Outlet and be rendered.

Navigation.js
// Navigation.js

import {Link, Outlet} from 'react-router-dom';          // MODIFIED

const Navigation = () => {

    return(
        <>
            <nav>
                <ul>
                    <li><Link to="/chocolates">All Chocolates</Link></li>
                    <li><Link to="/chocolates/new">Add New Chocolate</Link></li>
                </ul>
            </nav>
            <Outlet/>                                       // ADDED
        </>
    )

}

Now our chocolate components are rendered correctly on the page.

Redirecting to Another Page

React Router doesn't just give us the option to move around between different parts of our apps, it can even automate the process for us. This is going to be pretty useful as we currently have a bit of a UX issue when we add a new chocolate. We can add the details to the form and click the button, which sends the new chocolate off to the dataabse, but we're left staring at the form with no indication that anything happened. It would be great if we could head back over to the list of chocolates and see our tasty new snack in the list.

Help is at hand in the form of the useNavigate hook provided by React Router. This hook returns a function which can be used to redirect the app to a given route without the need for the user to click a link. We import it into ChocolateForm, call teh hook and store the returned function in a variable.

ChocolateForm.js
// ChocolateForm.js

import {useNavigate} from 'react-router-dom';       // ADDED

const ChocolateForm = ({estates, postChocolate}) => {

    const navigate = useNavigate();             // ADDED

    // ...

When we want to redirect our application we call the function and pass it a route. In our case we want to redirect the user back to the chocolate list after submitting a new chocolate.

ChocolateForm.js
// ChocolateForm.js

const handleFormSubmit = (event) => {
    // ...
    navigate("/chocolates");            // ADDED
}

Now our user is redirected to the list of all chocolates, which will include the new chocolate at the bottom. This can be a useful tool, but has its limitations. We can only use the hook in a component which is associated with a path and can only redirect to a route managed by router. This means we can't redirect in to or out of the structure defined by our router object, but can redirect inside of it easily.