Skip to main content

React Router v5: The Complete Guide

Share:

React Design Patterns and Best Practices - Second Edition

⚛️ Writing good React is just writing good JavaScript.

React Router is the de facto standard routing library for React. When you need to navigate through a React application with multiple views, you’ll need a router to manage the URLs. React Router takes care of that, keeping your application UI and the URL in sync.

This tutorial introduces you to React Router v5 and a whole lot of things you can do with it.

Introduction

React is a popular library for creating single-page applications (SPAs) that are rendered on the client side. An SPA might have multiple views (aka pages), and unlike conventional multi-page apps, navigating through these views shouldn’t result in the entire page being reloaded. Instead, we want the views to be rendered inline within the current page. The end user, who’s accustomed to multi-page apps, expects the following features to be present in an SPA:

  • Each view should have a URL that uniquely specifies that view. This is so that the user can bookmark the URL for reference at a later time. For example, www.example.com/products.
  • The browser’s back and forward button should work as expected.
  • Dynamically generated nested views should preferably have a URL of their own too — such as example.com/products/shoes/101, where 101 is the product ID.

Routing is the process of keeping the browser URL in sync with what’s being rendered on the page. React Router lets you handle routing declaratively. The declarative routing approach allows you to control the data flow in your application, by saying “the route should look like this”:

<Route path="/about">
  <About />
</Route>

You can place your <Route> component anywhere you want your route to be rendered. Since <Route>, <Link> and all the other React Router APIs that we’ll be dealing with are just components, you can easily get up and running with routing in React.

Note: there’s a common misconception that React Router is an official routing solution developed by Facebook. In reality, it’s a third-party library that’s widely popular for its design and simplicity.

Overview

This tutorial is divided into different sections. First, we’ll set up React and React Router using npm. Then we’ll jump right into some React Router basics. You’ll find different code demonstrations of React Router in action. The examples covered in this tutorial include:

  • basic navigational routing
  • nested routing
  • nested routing with path parameters
  • protected routing

All the concepts connected with building these routes will be discussed along the way.

The entire code for the project is available on this GitHub repo.

Let’s get started!

Setting up React Router

To follow along with this tutorial, you’ll need a recent version of Node installed on your PC. If this isn’t the case, then head over to the Node home page and download the correct binaries for your system. Alternatively, you might consider using a version manager to install Node. We have a tutorial on using a version manager here.

Node comes bundled with npm, a package manager for JavaScript, with which we’re going to install some of the libraries we’ll be using. You can learn more about using npm here.

You can check that both are installed correctly by issuing the following commands from the command line:

node -v
> 12.19.0

npm -v
> 6.14.8

With that done, let’s start off by creating a new React project with the Create React App tool. You can either install this globally, or use npx, like so:

npx create-react-app react-router-demo

When this has finished, change into the newly created directory:

cd react-router-demo

The React Router library comprises three packages: react-router, react-router-dom, and react-router-native. The core package for the router is react-router, whereas the other two are environment specific. You should use react-router-dom if you’re building a website, and react-router-native if you’re in a mobile app development environment using React Native.

Use npm to install react-router-dom:

npm install react-router-dom

Then start the development server with this:

npm run start

Congratulations! You now have a working React app with React Router installed. You can view the app running at http://localhost:3000/.

React Router Basics

Now let’s familiarize ourselves with a basic React Router setup. To do this, we’ll make an app with three separate views: Home, Category and Products.

The Router Component

The first thing we’ll need to do is to wrap our <App> component in a <Router> component (provided by React Router). Since we’re building a browser-based application, we can use two types of router from the React Router API:

The primary difference between them is evident in the URLs they create:

// <BrowserRouter>
http://example.com/about

// <HashRouter>
http://example.com/#/about

The <BrowserRouter> is the more popular of the two because it uses the HTML5 History API to keep your UI in sync with the URL, whereas the <HashRouter> uses the hash portion of the URL (window.location.hash). If you need to support legacy browsers that don’t support the History API, you should use <HashRouter>. Otherwise <BrowserRouter> is the better choice for most use cases. You can read more about the differences here.

So, let’s import the BrowserRouter component and wrap it around the App component:

// src/index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

The above code creates a history instance for our entire <App> component. Let’s look at what that means.

A Little Bit of History

The history library lets you easily manage session history anywhere JavaScript runs. A history object abstracts away the differences in various environments and provides a minimal API that lets you manage the history stack, navigate, and persist state between sessions. — React Training docs

Each <Router> component creates a history object that keeps track of the current location (history.location) and also the previous locations in a stack. When the current location changes, the view is re-rendered and you get a sense of navigation. How does the current location change? The history object has methods such as history.push and history.replace to take care of that. The history.push method is invoked when you click on a <Link> component, and history.replace is called when you use a <Redirect>. Other methods — such as history.goBack and history.goForward — are used to navigate through the history stack by going back or forward a page.

Moving on, we have Links and Routes.

Link and Route Components

The <Route> component is the most important component in React Router. It renders some UI if the current location matches the route’s path. Ideally, a <Route> component should have a prop named path, and if the path name matches the current location, it gets rendered.

The <Link> component, on the other hand, is used to navigate between pages. It’s comparable to the HTML anchor element. However, using anchor links would result in a full page refresh, which we don’t want. So instead, we can use <Link> to navigate to a particular URL and have the view re-rendered without a refresh.

Now we’ve covered everything you need to make our app work. Update src/App.js as follows:

import React from "react";
import { Link, Route, Switch } from "react-router-dom";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const Category = () => (
  <div>
    <h2>Category</h2>
  </div>
);

const Products = () => (
  <div>
    <h2>Products</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
        </ul>
      </nav>

      { /* Route components are rendered if the path prop matches the current URL */}
      <Route path="/"><Home /></Route>
      <Route path="/category"><Category /></Route>
      <Route path="/products"><Products /></Route>
    </div>
  );
}

Here, we’ve declared the components for Home, Category and Products inside App.js. Although this is okay for now, when a component starts to grow bigger, it’s better to have a separate file for each component. As a rule of thumb, I usually create a new file for a component if it occupies more than 10 lines of code. Starting from the second demo, I’ll be creating a separate file for components that have grown too big to fit inside the App.js file.

Inside the App component, we’ve written the logic for routing. The <Route>‘s path is matched with the current location and a component gets rendered. Previously, the component that should be rendered was passed in as a second prop. However, recent versions of React Router have introduced a new route rendering pattern, whereby the component(s) to be rendered are children of the <Route>.

Here / matches both / and /category. Therefore, both the routes are matched and rendered. How do we avoid that? You should pass the exact prop to the <Route> with path='/':

<Route exact path="/">
  <Home />
</Route>

If you want a route to be rendered only if the paths are exactly the same, you should use the exact prop.

Nested Routing

To create nested routes, we need to have a better understanding of how <Route> works. Let’s look at that now.

As you can read on the React Router docs, the recommended method of rendering something with a <Route> is to use children elements, as shown above. There are, however, a few other methods you can use to render something with a <Route>. These are provided mostly for supporting apps that were built with earlier versions of the router before hooks were introduced:

  • component: when the URL is matched, the router creates a React element from the given component using React.createElement.
  • render: handy for inline rendering. The render prop expects a function that returns an element when the location matches the route’s path.
  • children: this is similar to render, in that it expects a function that returns a React component. However, children gets rendered regardless of whether the path is matched with the location or not.

Path and Match

The path prop is used to identify the portion of the URL that the router should match. It uses the Path-to-RegExp library to turn a path string into a regular expression. It will then be matched against the current location.

If the router’s path and the location are successfully matched, an object is created which is called a match object. The match object contains more information about the URL and the path. This information is accessible through its properties, listed below:

  • match.url: a string that returns the matched portion of the URL. This is particularly useful for building nested <Link> components.
  • match.path: a string that returns the route’s path string — that is, <Route path="">. We’ll be using this to build nested <Route> components.
  • match.isExact: a Boolean that returns true if the match was exact (without any trailing characters).
  • match.params: an object containing key/value pairs from the URL parsed by the Path-to-RegExp package.

Implicit Passing of Props

Note that when using the component prop to render a route, the match, location and history route props are implicitly passed to the component. When using the newer route rendering pattern, this is not the case.

For example, take this component:

const Home = (props) => {
  console.log(props);

  return (
    <div>
      <h2>Home</h2>
    </div>
  );
};

Now render the route like so:

<Route exact path="/" component={Home} />

This will log the following:

{
  history: { ... }
  location: { ... }
  match: { ... }
}

But now instead render the route like so:

<Route exact path="/"><Home /></Route>

This will log the following:

{}

This might seem disadvantageous at first, but worry not! React v5.1 introduced several hooks to help you access what you need, where you need it. These hooks give us new ways to manage our router’s state and go quite some way to tidying up our components.

I’ll be using some of these hooks throughout this tutorial, but if you’d like a more in-depth look, check out the React Router v5.1 release announcement. Please also note that hooks were introduced in version 16.8 of React, so you’ll need to be on at least that version to use them.

The Switch Component

Before we head to the demo code, I want to introduce you to the Switch component. When multiple <Route>s are used together, all the routes that match are rendered inclusively. Consider this code from demo 1. I’ve added a new route to demonstrate why <Switch> is useful:

<Route exact path="/"><Home /></Route>
<Route path="/category"><Category /></Route>
<Route path="/products"><Products /></Route>
<Route path="/:id">
  <p>This text will render for any route other than '/'</p>
</Route>

If the URL is /products, all the routes that match the location /products are rendered. So, the <Route> with path /:id gets rendered along with the <Products> component. This is by design. However, if this is not the behavior you’re expecting, you should add the <Switch> component to your routes. With <Switch>, only the first child <Route> that matches the location gets rendered:

<Switch>
  <Route exact path="/"><Home /></Route>
  <Route path="/category"><Category /></Route>
  <Route path="/products"><Products /></Route>
  <Route path="/:id">
    <p>This text will render for any route other than those defined above</p>
  </Route>
</Switch>

The :id part of path is used for dynamic routing. It will match anything after the slash and make this value available in the component. We’ll see an example of this at work in the next section.

Now that we know all about the <Route> and <Switch> components, let’s add nested routes to our demo.

Dynamic Nested Routing

Earlier on, we created routes for /, /category and /products. But what if we wanted a URL in the form of /category/shoes?

Let’s start by updating src/App.js as follows:

import React from "react";
import { Link, Route, Switch } from "react-router-dom";
import Category from "./Category";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const Products = () => (
  <div>
    <h2>Products</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
        </ul>
      </nav>

      <Switch>
        <Route path="/"><Home /></Route>
        <Route path="/category"><Category /></Route>
        <Route path="/products"><Products /></Route>
      </Switch>
    </div>
  );
}

You’ll notice that we’ve moved Category into its own component. This is where our nested routes should go.

Let’s create Category.js now:

// src/Category.js

import React from "react";
import { Link, Route, useParams, useRouteMatch } from "react-router-dom";

const Item = () => {
  const { name } = useParams();

  return (
    <div>
      <h3>{name}</h3>
    </div>
  );
}

const Category = () => {
  const { url, path } = useRouteMatch();

  return (
    <div>
      <ul>
        <li>
          <Link to={`${url}/shoes`}>Shoes</Link>
        </li>
        <li>
          <Link to={`${url}/boots`}>Boots</Link>
        </li>
        <li>
          <Link to={`${url}/footwear`}>Footwear</Link>
        </li>
      </ul>
      <Route path={`${path}/:name`}>
        <Item />
      </Route>
    </div>
  );
};

export default Category;

Here, we’re using the useRouteMatch hook to gain access to the match object. As previously mentioned, match.url will be used for building nested links and match.path for nested routes. If you’re having trouble understanding the concept of match, console.log(useRouteMatch()) provides some useful information that might help to clarify it.

<Route path={`${path}/:name`}>
  <Item />
</Route>

This is our first proper attempt at dynamic routing. Instead of hard-coding the routes, we’ve used a variable within the path prop. :name is a path parameter and catches everything after category/ until another forward slash is encountered. So, a path name like products/running-shoes will create a params object as follows:

{
  name: "running-shoes";
}

To access this value within the <Item> component, we’re using the useParams hook, which returns an object of key/value pairs of URL parameters.

Try this out in your browser. The Category section should now have three sub-sections, each with their own route.

Nested Routing With Path Parameters

Let’s complicate things a bit more, shall we? A real-world router will have to deal with data and display it dynamically. Let’s assume we have some product data returned by an API in the following format:

const productData = [
  {
    id: 1,
    name: "NIKE Liteforce Blue Sneakers",
    description:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin molestie.",
    status: "Available",
  },
  {
    id: 2,
    name: "Stylised Flip Flops and Slippers",
    description:
      "Mauris finibus, massa eu tempor volutpat, magna dolor euismod dolor.",
    status: "Out of Stock",
  },
  {
    id: 3,
    name: "ADIDAS Adispree Running Shoes",
    description:
      "Maecenas condimentum porttitor auctor. Maecenas viverra fringilla felis, eu pretium.",
    status: "Available",
  },
  {
    id: 4,
    name: "ADIDAS Mid Sneakers",
    description:
      "Ut hendrerit venenatis lacus, vel lacinia ipsum fermentum vel. Cras.",
    status: "Out of Stock",
  },
];

Let’s also assume that we need to create routes for the following paths:

  • /products: this should display a list of products.
  • /products/:productId: if a product with the :productId exists, it should display the product data, and if not, it should display an error message.

Create a new file src/Products.js and add the following (making sure to copy in the product data from above):

import React from "react";
import { Link, Route, useRouteMatch } from "react-router-dom";
import Product from "./Product";

const Products = ({ match }) => {
  const productData = [ ... ];
  const { url } = useRouteMatch();

  /* Create an array of `<li>` items for each product */
  const linkList = productData.map((product) => {
    return (
      <li key={product.id}>
        <Link to={`${url}/${product.id}`}>{product.name}</Link>
      </li>
    );
  });

  return (
    <div>
      <div>
        <div>
          <h3>Products</h3>
          <ul>{linkList}</ul>
        </div>
      </div>

      <Route path={`${url}/:productId`}>
        <Product data={productData} />
      </Route>
      <Route exact path={url}>
        <p>Please select a product.</p>
      </Route>
    </div>
  );
};

export default Products;

First, we use the useRouteMatch hook to grab the URL from the match object. Then we build a list of <Links> components using the id property from each of our products, which we store in a linkList variable.

The first route uses a variable in the path prop which corresponds to that of the product ID. When it matches, we render out the <Product> component (which we’ll define in a minute), passing it our product data:

<Route path={`${url}/:productId`}>
  <Product data={productData} />
</Route>

The second route has an exact prop, so will only render when the URL is /products and nothing is selected.

Now, here’s the code for the <Product> component. You’ll need to create this file at src/Product.js:

import React from "react";
import { useParams } from "react-router-dom";

const Product = ({ data }) => {
  const { productId } = useParams();
  const product = data.find(p => p.id === Number(productId));
  let productData;

  if (product) {
    productData = (
      <div>
        <h3> {product.name} </h3>
        <p>{product.description}</p>
        <hr />
        <h4>{product.status}</h4>
      </div>
    );
  } else {
    productData = <h2> Sorry. Product doesn't exist </h2>;
  }

  return (
    <div>
      <div>{productData}</div>
    </div>
  );
};

export default Product;

The find method is used to search the array for an object with an ID property that equals match.params.productId. If the product exists, the productData is displayed. If not, a “Product doesn’t exist” message is rendered.

Finally, update your <App> component as follows:

import React from "react";
import { Link, Route, Switch } from "react-router-dom";
import Category from "./Category";
import Products from "./Products";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
        </ul>
      </nav>

      <Switch>
        <Route exact path="/"><Home /></Route>
        <Route path="/category"><Category /></Route>
        <Route path="/products"><Products /></Route>
      </Switch>
    </div>
  );
}

Now when you visit the application in the browser and select “Products”, you’ll see a sub-menu rendered, which in turn displays the product data.

Have a play around with the demo. Assure yourself that everything works and that you understand what’s happening in the code.

Protecting Routes

A common requirement for many modern web apps is to ensure that only logged-in users can access certain parts of the site. In this next section, we’ll look at how to implement a protected route, so that if someone tries to access /admin, they’ll be required to log in.

However, there are a couple of aspects of React Router that we need to cover first.

The Redirect Component

As with server-side redirects, React Router’s Redirect component will replace the current location in the history stack with a new location. The new location is specified by the to prop. Here’s how we’ll be using <Redirect>:

<Redirect to={{pathname: '/login', state: { from: location }}}

So, if someone tries to access the /admin route while logged out, they’ll be redirected to the /login route. The information about the current location is passed via the state prop, so that if the authentication is successful, the user can be redirected back to the page they were originally trying to access.

Custom Routes

A custom route is a fancy way of describing a route nested inside a component. If we need to make a decision whether a route should be rendered or not, writing a custom route is the way to go.

Create a new file PrivateRoute.js in the src directory and add the following content:

import React from "react";
import { Redirect, Route, useLocation } from "react-router-dom";
import { fakeAuth } from './Login';

const PrivateRoute = ({ component: Component, ...rest }) => {
  const location = useLocation();

  return (
    <Route {...rest}>
      {fakeAuth.isAuthenticated === true ?
        <Component />
      :
        <Redirect to={{ pathname: "/login", state: { from: location } }} />
      }
    </Route>
  );
};

export default PrivateRoute;

As you can see, in the function definition we’re destructuring the props we receive into a Component prop and a rest prop. The Component prop will contain whichever component our <PrivateRoute> is protecting (in our case, Admin). The rest prop will contain any other props we’ve been passed.

We then return a <Route> component, which renders either the protected component or redirects us to our /login route, depending on whether or not the user is logged in. This is determined here by a fakeAuth.isAuthenticated property, which is imported from the <Login> component.

The good thing about this approach is that it’s evidently more declarative and <PrivateRoute> is reusable.

Important Security Notice

In a real-world app, you need to validate any request for a protected resource on your server. This is because anything that runs in the client can potentially be reverse engineered and tampered with. For example, in the above code one can just open React’s dev tools and change the value of isAuthenticated, thus gaining access to the protected area.

Authentication in a React app is worthy of a tutorial of its own, but one way to implement it would be using JSON Web Tokens. For example, you could have an endpoint on your server which accepts a username and password combination. When it receives these (via Ajax), it checks to see if the credentials are valid. If so, it responds with a JWT, which the React app saves (for example in sessionStorage), and if not, it sends a 401 Unauthorized response back to the client.

Assuming a successful login, the client would then send the JWT as a header along with any request for a protected resource. This would then be validated by the server before it sent a response.

When storing passwords, the server would not store them in plaintext. Rather, it would encrypt them — for example, using bcryptjs.

Implementing the Protected Route

Now let’s implement our protected route. Alter src/App.js like so:

import React from "react";
import { Link, Route, Switch } from "react-router-dom";
import Category from "./Category";
import Products from "./Products";
import Login from './Login';
import PrivateRoute from "./PrivateRoute";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const Admin = () => (
  <div>
    <h2>Welcome admin!</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
          <li>
            <Link to="/admin">Admin area</Link>
          </li>
        </ul>
      </nav>

      <Switch>
        <Route exact path="/"><Home /></Route>
        <Route path="/category"><Category /></Route>
        <Route path="/products"><Products /></Route>
        <Route path="/login"><Login /></Route>
        <PrivateRoute path="/admin" component={Admin} />
      </Switch>
    </div>
  );
}

As you can see, we’ve added an <Admin> component to the top of the file and are including our <PrivateRoute> within the <Switch> component. As mentioned previously, this custom route renders the <Admin> component if the user is logged in. Otherwise, the user is redirected to /login.

Finally, here’s the code for the Login component:

import React, { useState } from "react";
import { Redirect, useLocation } from "react-router-dom";

export default function Login() {
  const { state } = useLocation();
  const { from } = state || { from: { pathname: "/" } };
  const [redirectToReferrer, setRedirectToReferrer] = useState(false);

  const login = () => {
    fakeAuth.authenticate(() => {
      setRedirectToReferrer(true);
    });
  };

  if (redirectToReferrer) {
    return <Redirect to={from} />;
  }

  return (
    <div>
      <p>You must log in to view the page at {from.pathname}</p>
      <button onClick={login}>Log in</button>
    </div>
  );
}

/* A fake authentication function */
export const fakeAuth = {
  isAuthenticated: false,
  authenticate(cb) {
    this.isAuthenticated = true;
    setTimeout(cb, 100);
  }
};

By now, there’s hopefully nothing too tricky going on here. We use the useLocation hook to access the router’s location prop, from which we grab the state property. We then use object destructuring to get a value for the URL the user was trying to access before being asked to log in. If this isn’t present, we set it to { pathname: "/" }.

We then use React’s useState hook to initialize a redirectToReferrer property to false. Depending on the value of this property, the user is either redirected to where they were going (that is, the user is logged in), or the user is presented with a button to log them in.

Once the button is clicked, the fakeAuth.authenticate method is executed, which sets fakeAuth.isAuthenticated to true and (in a callback function) updates the value of redirectToReferrer to true. This causes the component to re-render and the user to be redirected.

Working Demo

Let’s fit the puzzle pieces together, shall we? Here’s the final demo of the application we built using React router.

Summary

As you’ve seen in this article, React Router is a powerful library that complements React for building better, declarative routes. Unlike the prior versions of React Router, in v5, everything is “just components”. Moreover, the new design pattern perfectly fits into the React way of doing things.

In this tutorial, we learned:

  • how to set up and install React Router
  • the basics of routing and some essential components such as <Router>, <Route> and <Link>
  • how to create a minimal router for navigation and nested routes
  • how to build dynamic routes with path parameters
  • how to work with React Router’s hooks and its newer route rendering pattern

Finally, we learned some advanced routing techniques for creating the final demo for protected routes.

Founder of Storylens - A no-code website building platform based on JavaScript components.

I write clean, readable and modular code. I love learning new technologies that bring efficiencies and increased productivity to my workflow.

New books out now!

Get practical advice to start your career in programming!


Master complex transitions, transformations and animations in CSS!