The React Mega-Tutorial, Chapter 4: Routing and Page Navigation

Posted by
on under

React is a Single-Page Application (SPA) framework, which means that from the point of view of the browser, only one web page is ever downloaded. Once that page is active, all the application state changes will happen through JavaScript events, without the browser having to fetch new web pages from the server. How then, can the application support page navigation?

The complete course, including videos for every chapter is available to order from my courses site. Ebook and paperback versions of this course are also available from Amazon. Thank you for your support!

For your reference, here is the complete list of articles in this series:

The React Router package, which you installed in Chapter 2, implements a complete page navigation system for SPAs. In this chapter you will learn how to create client-side routes and navigate between them.

Creating Page Components

The concept of a page, in the strict browser sense, does not really apply in an SPA, since SPAs only have one page. In an SPA, pages are just top-level application states that dictate how the application renders in the browser. As with traditional pages, each page in an SPA can be associated with paths such as /login or /user/susan, but these paths are managed by the client application and never reach the server.

The React-Router package keeps track of these page states, and automatically updates the address bar of the browser with the appropriate URL path. It also takes control of the Back and Forward buttons of the browser and makes them work as the user expects.

To help have a sane application structure, the top-level components that map to the logical pages of the application will be written in a separate directory, called pages. Create this directory now:

mkdir src/pages

The default page for this application is going to be the page that displays the post feed for the user. Let's move the Body component, which is currently in App, to a new FeedPage component, stored in src/pages/FeedPage.js.

src/pages/FeedPage.js: the Feed page

import Body from '../components/Body';
import Posts from '../components/Posts';

export default function FeedPage() {
  return (
    <Body sidebar>
      <Posts />
    </Body>
  );
}

Note how the import path for components now uses ../components/. This is because the path is relative to the location of the importing source file, which is in src/pages.

The second most important page in this application is going to be the Explore page, which will display blog posts from all the users in the system, and is intended as the place where users can discover other users to follow. Add a placeholder for this page in the ExplorePage component:

src/pages/ExplorePage.js: a placeholder for the explore page

import Body from '../components/Body';

export default function ExplorePage() {
  return (
    <Body sidebar>
      <h1>Explore</h1>
      <p>TODO</p>
    </Body>
  );
}

Even though the application isn't ready to have a login page yet, also create a placeholder for the LoginPage component, so that you can later test navigation between three different pages.

src/pages/LoginPage.js: a placeholder for the login page

import Body from '../components/Body';

export default function LoginPage() {
  return (
    <Body>
      <h1>Login form</h1>
      <p>TODO</p>
    </Body>
  );
}

With the help of the React-Router package, the App component can now implement routing for these three pages. Below is the new version of App, using several new routing components.

src/App.js: Page routing

import Container from 'react-bootstrap/Container';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Header from './components/Header';
import FeedPage from './pages/FeedPage';
import ExplorePage from './pages/ExplorePage';
import LoginPage from './pages/LoginPage';

export default function App() {
  return (
    <Container fluid className="App">
      <BrowserRouter>
        <Header />
        <Routes>
          <Route path="/" element={<FeedPage />} />
          <Route path="/explore" element={<ExplorePage />} />
          <Route path="/login" element={<LoginPage />} />
          <Route path="*" element={<Navigate to="/" />} />
        </Routes>
      </BrowserRouter>
    </Container>
  );
}

The React-Router library is called react-router-dom. There are four components provided by this library that are used above.

The BrowserRouter component adds routing support to the application. This component must be added very high in the component hierarchy, as it must be a parent to all the routing logic in the application. In terms of rendering, this component transparently renders its children without rendering anything itself, so it can be added conveniently as a parent near the top of the JSX tree.

Routes is a component that needs to be inserted in the place in the component tree where the contents need to change based on the current page. As an analogy, you can think of Routes as a routing equivalent to the switch statement in JavaScript, or a long chain or if-then-else statements.

Route is used to define a route inside the Routes component. The path attribute defines the path portion of the URL for the route, and the element attribute specifies what contents are associated with the route. Following the switch statement analogy, this component is equivalent to the case statement.

Navigate is a special component that allows to redirect from one route to another. The fourth Route component in the listing above has a path set to *, which works as a catch-all route for any URLs that are not matched by the routes declared above it. The element attribute in this route uses Navigate to redirect all these unknown URLs to the root URL.

With this version of the application, the address bar, Back and Forward buttons of your browser are connected to the application, and routing is functional. If you type http://localhost:3000/login in the address bar of your browser, the application will load and automatically render the login page placeholder added earlier. The same will occur if you use /explore in the path portion of the URL.

The two links in the sidebar are standard browser links that are not connected to React-Router yet, so they trigger a full page reload. Ignoring the inefficiency this causes (which will soon be fixed), they should allow you to switch between the feed and explore pages.

After you've moved between the three pages a few times, try the Back and Forward buttons in your browser to see how React-Router makes the SPA behave like a normal multipage website.

Implementing Links

As noted above, the two links in the sidebar are working in the sense that they allow you to navigate between pages, but they are inefficient, because they are standard browser links that reload the entire React application every time they are clicked. For a single-page application, the routing between pages should be handled internally in JavaScript, without reaching the browser's own page navigation system.

The React-Router package provides the Link and NavLink components to aid in the generation of SPA-friendly links. The difference between them is that Link is just a regular link, while NavLink extends the behavior of a link with the ability to become "active" when its URL matches the current page, allowing the application to change the styling. In a navigation bar such as the Sidebar component, NavLink makes the most sense to use, because the currently active page can be highlighted.

The existing links in the sidebar use the Nav.Link component from React-Bootstrap. This component has a very similar name, except for the dot between Nav and Link, but these two components are completely different. In the current version of the sidebar, a link is created with the following syntax:

 <Nav.Link href="/">Feed</Nav.Link>

How can this be combined with the NavLink component from React-Router to generate an SPA link? React-Bootstrap recognizes that many times its components need to be integrated with components from other libraries, so Nav.Link (as well as many others), have the as attribute to specify a different base component. This makes it possible to use a React-Bootstrap component as a wrapper to a component from another library such as React-Router.

Here is how the above link can be adapted to work with React-Router's NavLink, while still being compatible with Bootstrap:

<Nav.Link as={NavLink} to="/">Feed</Nav.Link>

Note how the href attribute of Nav.Link has been renamed to to, because now this component will render as React-Router's NavLink, which uses to instead of href. With this solution, the Bootstrap CSS classes associated with navigation links are preserved, but they are applied to the React-Router NavLink component.

Here is the React-Router version of Sidebar:

src/components/Sidebar.js: React-Router enabled sidebar

import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { NavLink } from 'react-router-dom';

export default function Sidebar() {
  return (
    <Navbar sticky="top" className="flex-column Sidebar">
      <Nav.Item>
        <Nav.Link as={NavLink} to="/" end>Feed</Nav.Link>
      </Nav.Item>
      <Nav.Item>
        <Nav.Link as={NavLink} to="/explore">Explore</Nav.Link>
      </Nav.Item>
    </Navbar>
  );
}

The NavLink component recognizes when the current page URL maintained by React-Router matches its own link address, and in that case it considers the link "active". Note the end attribute that was added to the feed link. This attribute tells React-Router that the link should be considered active only when the URL matches exactly. Without end, the link would be considered active when the URL starts with the path given in the to attribute, which means that for this root URL the link would be marked as active for every page.

For an active link, NavLink adds the active class name to its <a> element. This class name can be used in src/index.css to create new style for the active link. Add the following class definition at the bottom of index.css:

src/index.css: Active page style for navigation links

... // <-- no changes to existing styles

.Sidebar .nav-item .active {
  background-color: #def;
}

With this new CSS definition, you can navigate between the feed and explore pages, and whichever of the two links is active renders with a light blue background color, as shown in Figure 4.1.

Figure 4.1: Navigation link styling

Pages with Dynamic Parameters

For most web applications, some pages need route URLs that have placeholders in sections of the path. Consider having a profile page for each user. The most convenient way to define the route for this page is to include the user ID or name in the path itself. In Microblog, the profile page for a given user is going to be /user/{username}.

To define a route with a dynamic section, the path attribute of the Route component uses a special syntax with a colon prefix:

<Route path="/user/:username" element={<UserPage />} />

The : denotes that section of the path as a placeholder that matches any value. The component referenced by the element attribute or any of its children can use the useParams() function to access the dynamic parameters of the current URL as an object.

This is the first time you encounter a function that starts with the word use. In React, "use" functions are called hooks. Hook functions are special in React because they provide access to application state. React includes a number of hooks, most of which you'll encounter in later chapters. Many libraries for React also provide their own hooks, such as the useParams() hook from React-Router. Applications can create custom hooks as well, and you will also learn about this later.

Let's add a simple version of the user profile page to the application, which for now will just show the username. This is going to be the UserPage component, stored in src/pages/UserPage.js.

src/pages/UserPage.js: a simple user profile page

import { useParams } from 'react-router-dom';
import Body from '../components/Body';

export default function UserPage() {
  const { username } = useParams();

  return (
    <Body sidebar>
      <h1>{username}</h1>
      <p>TODO</p>
    </Body>
  );
}

The listing below shows the updated App component with the user profile page.

src/App.js: User profile page with a dynamic parameter

import Container from 'react-bootstrap/Container';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Header from './components/Header';
import FeedPage from './pages/FeedPage';
import ExplorePage from './pages/ExplorePage';
import UserPage from './pages/UserPage';
import LoginPage from './pages/LoginPage';

export default function App() {
  return (
    <Container fluid className="App">
      <BrowserRouter>
        <Header />
        <Routes>
          <Route path="/" element={<FeedPage />} />
          <Route path="/explore" element={<ExplorePage />} />
          <Route path="/user/:username" element={<UserPage />} />
          <Route path="/login" element={<LoginPage />} />
          <Route path="*" element={<Navigate to="/" />} />
        </Routes>
      </BrowserRouter>
    </Container>
  );
}

As with the login page, the new user profile page is currently not linked from any other part of the application, so the only way to view it is by typing a matching URL in the browser's address bar. Figure 4.2 shows the user page that corresponds to the http://localhost:3000/user/susan URL.

Figure 4.2: User profile page with a dynamic parameter

Chapter Summary

  • Use React-Router to create client-side routes in your React application.
  • Following on the idea of having nicely organized code, create a subdirectory for page-level components.
  • Create each logical page of the application as a separate page-level component that can be defined as a route.
Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

4 comments
  • #1 Maria said

    I had an issue where the 'Feed' link was always active no matter what page I was on; the solution was to add the prop 'end' to the link in Sidebar.js, as so:

    <Nav.Link as={NavLink} end to="/">Feed</Nav.Link>

    I think this is a fairly new update (the prop was 'exact' before), and I wanted to point it out in case any future visitors have the same issue.

    (And thank you for your tutorials, they are indispensable!)

  • #2 Miguel Grinberg said

    @Maria: Yes, looks like this is a breaking change introduced in react-router 6.4.0, released a few days ago. Adding end is the correct solution, I will update the tutorial.

  • #3 Travis Prosser said

    FYI for anyone coming to this after November 2022, this behavior with 'end' on the root path '/' appears to have been an untinentional regression which was corrected in release 6.4.3 (https://github.com/remix-run/react-router/releases/tag/react-router%406.4.3) via PR #9497 (https://github.com/remix-run/react-router/pull/9497).

    So this appears to be no longer needed outside of a relatively narrow version range of react-router-dom.

  • #4 Miguel Grinberg said

    @Travis: I believe it makes sense to use end here, regardless of how you have observed that this property is not needed on certain version or version range. The documentation does not mention that the root URL has any special handling, so adding end is the proper way to prevent the root links from always being marked as active. This is exactly what you would do if you had subsections, each with its own sub-root URL, so I think it makes sense to do it for the root URL as well, if only for consistency.

Leave a Comment