Routing

What's in a URL?

A URL is a reference to a web resource. A typical URL looks something like this:


While a combination of the protocol and the hostname direct us to a certain website, it's the pathname that references a specific resource on that site. Another way to think about it: the pathname references a specific location in our application.

For example, consider a URL for some music website:


https://example.com.com/artists/87589/albums/1758221

This location refers to a specific album by an artist. The URL contains identifiers for both the artist and album desired:


example.com/artists/:artistId/albums/:albumId

We can think of the URL as being an external keeper of state, in this case the album the user is viewing. By storing pieces of app state up at the level of the browser's location, we can enable users to bookmark the link, refresh the page, and share it with others.

In a traditional web application with minimal JavaScript, the request flow for this page might look like this:

  1. Browser makes a request to the server for this page.
  2. The server uses the identifiers in the URL to retrieve data about the artist and the album from its database.
  3. The server populates a template with this data.
  4. The server returns this populated HTML document along with any other assets like CSS and images.
  5. The browser renders these assets.

When using a rich JavaScript framework like React, we want React to generate the page. So an evolution of that request flow using React might look like this:

  1. Browser makes a request to the server for this page.
  2. The server doesn't care about the pathname. Instead, it just returns a standard index.html that includes the React app and any static assets.
  3. The React app mounts.
  4. The React app extracts the identifiers from the URL and uses these identifiers to make an API call to fetch the data for the artist and the album. It might make this call to the same server.
  5. The React app renders the page using data it received from the API call.

Projects elsewhere in the book have mirrored this second request flow. One example is the timers app in "Components & Servers." The same server.js served both the static assets (the React app) and an API that fed that React app data.

This initial request flow for React is slightly more inefficient than the first. Instead of one round-trip from the browser to the server, there will be two or more: One to fetch the React app and then however many API calls the React app has to make to get all the data it needs to render the page.

However, the gains come after the initial page load. The user experience of our timers app with React is much better than it would be without. Without JavaScript, each time the user wanted to stop, start, or edit a timer, their browser would have to fetch a brand new page from the server. This adds noticeable delay and an unpleasant "blink" between page loads.

Single-page applications (SPAs) are web apps that load once and then dynamically update elements on the page using JavaScript. Every React app we've built so far has been a type of SPA.

So we've seen how to use React to make interface elements on a page fluid and dynamic. But other apps in the book have had only a single location. For instance, the product voting app had a single view: the list of products to vote on. What if we wanted to add a different page, like a product view page at the location /products/:productId? This page would use a completely different set of components.

Back to our music website example, imagine the user is looking at the React-powered album view page. They then click on an "Account" button at the top right of the app to view their account information. A request flow to support this might look like:

  1. User clicks on the "Account" button which is a link to /account.
  2. Browser makes a request to /account.
  3. The server, again, doesn't care about the pathname. Again, it returns the same index.html that includes the full React app and static assets.
  4. The React app mounts. It checks the URL and sees that the user is looking at the /accounts page.
  5. The top-level React component, say App, might have a switch for what component to render based on the URL. Before, it was rendering AlbumView. But now it renders AccountView.
  6. The React app renders and populates itself with an API request to the server (say /api/account).

This approach works and we can see examples of it across the web. But for many types of applications, there's a more efficient approach.

When the user clicks on the "Account" button, we could prevent the browser from fetching the next page from /account. Instead, we could instruct the React app to switch out the AlbumView component for the AccountView component. In full, that flow would look like this:

  1. User visits https://example.com.com/artists/87589/albums/1758221.
  2. The server delivers the standard index.html that includes the React app and assets.
  3. The React app mounts and populates itself by making an API call to the server.
  4. User clicks on the "Account" button.
  5. The React app captures this click event. React updates the URL to https://example.com/account and re-renders.
  6. When the React app re-renders, it checks the URL. It sees the user is viewing /account and it swaps in the AccountView component.
  7. The React app makes an API call to populate the AccountView component.

When the user clicks on the "Account" button, the browser already contains the full React app. There's no need to have the browser make a new request to fetch the same app again from the server and re-mount it. The React app just needs to update the URL and then re-render itself with a new component-tree (AccountView).

This is the idea of a JavaScript router. As we'll see first hand, routing involves two primary pieces of functionality: (1) Modifying the location of the app (the URL) and (2) determining what React components to render at a given location.

There are many routing libraries for React, but the community's clear favorite is React Router. React Router gives us a wonderful foundation for building rich applications that have hundreds or thousands of React components across many different views and URLs.

React Router's core components

For modifying the location of an app, we use links and redirects. In React Router, links and redirects are managed by two React components, Link and Redirect.

For determining what to render at a given location, we also use two React Router components, Route and Switch.

To best understand React Router, we'll start out by building basic versions of React Router's core components. In doing so, we'll get a feel for what routing looks like in a component-driven paradigm.

We'll then swap out our components for those provided by the react-router library. We'll explore a few more components and features of the library.

In the second half of the chapter, we'll see React Router at work in a slightly larger application. The app we build will have multiple pages with dynamic URLs. The app will communicate with a server that is protected by an API token. We'll explore a strategy for handling logging and logging out inside a React Router app.

React Router v4

The latest version of React Router, v4, is a major shift from its predecessors. The authors of React Router state that the most compelling aspect of this version of the library is that it's "just React."

We agree. And while v4 was just released at the time of writing, we find its paradigm so compelling that we wanted to ensure we covered v4 as opposed to v3 here in the book. We believe v4 will be rapidly adopted by the community.

Because v4 is so new, it's possible the next few months will see some changes. But the essence of v4 is settled, and this chapter focuses on those core concepts.

Building the components of react-router

The completed app

All the example code for this chapter is inside the folder routing in the code download. We'll start off with the basics app:


$ cd routing/basics

Taking a look inside this directory, we see that this app is powered by create-react-app:


$ ls
README.md
nightwatch.json
package.json
public/
src/
tests/

If you need a refresher on create-react-app, refer to the chapter "Using Webpack with create-react-app."

Our React app lives inside src/:


$ ls src
App.css
App.js
SelectableApp.js
complete/
index-complete.js
index.css
index.js
logo.svg

complete/ contains the completed version of App.js. The folder also contains each iteration of App.js that we build up throughout this section.

Install the npm packages:


$ npm i

At the moment, index.js is loading index-complete.js. index-complete.js uses SelectableApp to give us the ability to toggle between the app's various iterations. SelectableApp is just for demo purposes.

If we boot the app, we'll see the completed version:


$ npm start

The app consists of three links. Clicking on a link displays a blurb about the selected body of water below the application:


The completed app

Notice that clicking on a link changes the location of the app. Clicking on the link /atlantic updates the URL to /atlantic. Importantly, the browser does not make a request when we click on a link. The blurb about the Atlantic Ocean appears and the browser's URL bar updates to /atlantic instantly.

Clicking on the link /black-sea displays a countdown. When the countdown finishes, the app redirects the browser to /.

The routing in this app is powered by the react-router library. We'll build a version of the app ourselves by constructing our own React Router components.

We'll be working inside the file App.js throughout this section.

Building Route

We'll start off by building React Router's Route component. We'll see what it does shortly.

Let's open the file src/App.js. Inside is a skeletal version of App. Below the import statement for React, we define a simple App component with two <a> tag links:


class App extends React.Component {
  render() {
    return (
      <div
        className='ui text container'
      >
        <h2 className='ui dividing header'>
          Which body of water?
        </h2>

        <ul>
          <li>
            <a href='/atlantic'>
              <code>/atlantic</code>
            </a>
          </li>
          <li>
            <a href='/pacific'>
              <code>/pacific</code>
            </a>
          </li>
        </ul>

        <hr />

        {/* We'll insert the Route components here */}
      </div>
    );
  }
}

We have two regular HTML anchor tags pointing to the paths /atlantic and /pacific.

Below App are two stateless functional components:


const Atlantic = () => (
  <div>
    <h3>Atlantic Ocean</h3>
    <p>
      The Atlantic Ocean covers approximately 1/5th of the
      surface of the earth.
    </p>
  </div>
);

const Pacific = () => (
  <div>
    <h3>Pacific Ocean</h3>
    <p>
      Ferdinand Magellan, a Portuguese explorer, named the ocean
      'mar pacifico' in 1521, which means peaceful sea.
    </p>
  </div>
);

These components render some facts about the two oceans. Eventually, we want to render these components inside App. We want to have App render Atlantic when the browser's location is /atlantic and Pacific when the location is /pacific.

Recall that index.js is currently deferring to index-complete.js to load the completed version of the app to the DOM. Before we can take a look at the app so far, we need to ensure index.js mounts the App component we're working on here in ./App.js instead.

Open up index.js. First, comment out the line that imports index-complete:


// [STEP 1] Comment out this line:
// leanpub-start-insert
// import "./index-complete";
// leanpub-end-insert

As in other create-react-app apps, the mounting of the React app to the DOM will take place here in index.js. Let's un-comment the line that mounts App:


// [STEP 2] Un-comment this line:
// leanpub-start-insert
ReactDOM.render(<App />, document.getElementById("root"));
// leanpub-end-insert

From the root of the project's folder, we can boot the app with the start command:


$ npm start

We see the two links rendered on the page. We can click on them and note the browser makes a page request. The URL bar is updated but nothing in the app changes:


We see neither Atlantic nor Pacific rendered, which makes sense because we haven't yet included them in App. Despite this, it's interesting that at the moment our app doesn't care about the state of the pathname. No matter what path the browser requests from our server, the server will return the same index.html with the same exact JavaScript bundle.

This is a desirable foundation. We want our browser to load React in the same way in each location and defer to React on what to do at each location.

Let's have our app render the appropriate component, Atlantic or Pacific, based on the location of the app (/atlantic or /pacific). To implement this behavior, we'll write and use a Route component.

In React Router, Route is a component that determines whether or not to render a specified component based on the app's location. We'll need to supply Route with two arguments as props:

  • The path to match against the location
  • The component to render when the location matches path

Let's look at how we might use this component before we write it. In the render() function of our App component, we'll use Route like so:


        <ul>
          <li>
            <a href='/atlantic'>
              <code>/atlantic</code>
            </a>
          </li>
          <li>
            <a href='/pacific'>
              <code>/pacific</code>
            </a>
          </li>
        </ul>

        <hr />

        {/* leanpub-start-insert */}
        <Route path='/atlantic' component={Atlantic} />
        <Route path='/pacific' component={Pacific} />
        {/* leanpub-end-insert */}
      </div>
    );

Route, like everything else in React Router, is a component. The supplied path prop is matched against the browser's location. If it matches, Route will return the component. If not, Route will return null, rendering nothing.

At the top of the file above App, let's write the Route component as a stateless function. We'll take a look at the code then break it down:


mport React from 'react';

// leanpub-start-insert
const Route = ({ path, component }) => {
  const pathname = window.location.pathname;
  if (pathname.match(path)) {
    return (
      React.createElement(component)
    );
  } else {
    return null;
  }
};
// leanpub-end-insert

class App extends React.Component {

We use the ES6 destructuring syntax to extract our two props, path and component, from the arguments:


const Route = ({ path, component }) => {

Next, we instantiate the pathname variable:


  const pathname = window.location.pathname;

Inside a browser environment, window.location is a special object containing the properties of the browser's current location. We grab the pathname from this object which is the path of the URL.

Last, if the path supplied to Route matches the pathname, we return the component. Otherwise, we return null: