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:
- Browser makes a request to the server for this page.
- The server uses the identifiers in the URL to retrieve data about the artist and the album from its database.
- The server populates a template with this data.
- The server returns this populated HTML document along with any other assets like CSS and images.
- 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:
- Browser makes a request to the server for this page.
- 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. - The React app mounts.
- 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.
- 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:
- User clicks on the "Account" button which is a link to
/account
. - Browser makes a request to
/account
. - 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. - The React app mounts. It checks the URL and sees that the user is looking at the
/accounts
page. - The top-level React component, say
App
, might have a switch for what component to render based on the URL. Before, it was renderingAlbumView
. But now it rendersAccountView
. - 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:
- User visits
https://example.com.com/artists/87589/albums/1758221
. - The server delivers the standard
index.html
that includes the React app and assets. - The React app mounts and populates itself by making an API call to the server.
- User clicks on the "Account" button.
- The React app captures this click event. React updates the URL to
https://example.com/account
and re-renders. - When the React app re-renders, it checks the URL. It sees the user is viewing
/account
and it swaps in theAccountView
component. - 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:
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 matchespath
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
: