Esteban Herrera Family man, #Java and #Javascript developer. #Swift, and #VR/#AR hobbyist. Like #books, #movies and still trying many things. eherrera.net

React Router DOM: Setup, essential components, and parameterized routes

11 min read 3256

React Router DOM

Recently, Yomi did a great job writing about advanced React Router concepts. But if you’re just starting out with React Router, that post was probably a little too much to wrap your head around.

Not to worry. In this post, I’ll get you started with the basics of the web version (React Router DOM).

Here’s what you’ll learn:

  • The concept of a router
  • How to setup and install React Router
  • The essential components of this framework
  • How to build routes with parameters, like /messages/10

The demo app for this post is available on CodeSandbox:

For reference, you can find the code of the final example on this GitHub repository.

Let’s get started.

What is a router?

Single-page applications (SPAs) rewrite sections of a page rather than loading entire new pages from a server.

Twitter is a good example of this type of application. When you click on a tweet, only the tweet’s information is fetched from the server. The page does not fully reload:

These applications are easy to deploy and greatly improve the user experience, among other advantages.

However, they also bring challenges.

One of them is browser history. As the application is contained in a single page, it cannot rely on the browser’s forward/back buttons per se. It needs something else.

Something that, according to the application’s state, changes the URL to push or replace URL history events within the browser. At the same time, it also needs to rebuild the application state from information contained within the URL.

On Twitter, for example, notice how the URL changes when a tweet is clicked:

And how a history entry is generated:

This is the job of a router.

A router allows your application to navigate between different components, changing the browser URL, modifying the browser history, and keeping the UI state in sync.

React is a popular library for building SPAs. However, as React focuses only on building user interfaces, it doesn’t have a built-in solution for routing.

React Router is the most popular routing library for React. It allows you define routes in the same declarative style:

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

But let’s not get ahead of ourselves. Let’s start by creating a sample project and setting up React Router.

Setting up React Router

I’m going to use Create React App to create a React app. You can install (or update) it with:

npm install -g create-react-app

You just need to have Node.js version 6 or superior installed.

Next, execute the following command:

create-react-app react-router-example

In this case, the directory react-router-example will be created. If you cd into it, you should see a structure similar to the following:

React Router includes three main packages:

  • react-router: This is the core package for the router
  • react-router-dom: It contains the DOM bindings for React Router. In other words, the router components for websites
  • react-router-native: It contains the React Native bindings for React Router. In other words, the router components for an app development environment using React Native

Since this is a web app, you’ll have to install react-router-dom:

npm install — save react-router-dom

At this point, you can execute:

npm start

A browser window will open http://localhost:3000/ and you should see something like this:

Now let’s create a simple SPA with React and React Router.

Using React Router

The React Router API is based on three components:

  • <Router>: The router that keeps the UI in sync with the URL
  • <Link>: Renders a navigation link
  • <Route>: Renders a UI component depending on the URL

<Router>

Only in some special cases you’ll have to use <Router> directly (for example when working with Redux), so the first thing you have to do is to choose a router implementation.

In a web application, you have two options:

If you’re going to target older browsers that don’t support the HTML5 History API, you should stick with <HashRouter>, which creates URLs with the following format:

http://localhost:3000/#/route/subroute

Otherwise, you can use <BrowserRouter>, which creates URLs with the following format:

http://localhost:3000/route/subroute

I’ll use <BrowserRouter>, so in src/index.js, I’m going to import this component from react-router-dom and use it to wrap the <App> component:

import React from 'react';
// ...
import { BrowserRouter } from 'react-router-dom';

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

It’s important to mention that a router component can only have one child element. For example, the following code:

// ...
ReactDOM.render(
  <BrowserRouter>
      <App />
      <div>Another child!</div>
  </BrowserRouter>
  , document.getElementById('root')
);
// ...

Throws this error message:

The main job of a <Router> component is to create a history object to keep track of the location (URL). When the location changes because of a navigation action, the child component (in this case <App>) is re-rendered.

Most of the time, you’ll use a <Link> component to change the location.

<Link>

Let’s create a navigation menu. Open src/App.css to add the following styles:

ul {
  list-style-type: none;
  padding: 0;
}

.menu ul {
  background-color: #222;
  margin: 0;
}

.menu li {
  font-family: sans-serif;
  font-size: 1.2em;
  line-height: 40px;
  height: 40px;
  border-bottom: 1px solid #888;
}
 
.menu a {
  text-decoration: none;
  color: #fff;
  display: block;
}

In scr/App.js, replace the last <p> element in the render() function so it looks like this:

render() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h1 className="App-title">Welcome to React</h1>
      </header>
      <div className="menu">
          <ul>
            <li> <Link to="/">Home</Link> </li>
            <li> <Link to="/messages">Messages</Link> </li>
            <li> <Link to="/about">About</Link> </li>
          </ul>
      </div>
    </div>
  );
}

Don’t forget to import the <Link> component at the top of the file:

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

In the browser, you should see something like this:

As you can see, this JSX code:

<ul>
<li> <Link to="/">Home</Link> </li>
<li> <Link to="/messages">Messages</Link> </li>
<li> <Link to="/about">About</Link> </li>
</ul>

Generates the following HTML code:

<ul>
<li> <a href="/">Home</a> </li>
<li> <a href="/messages">Messages</a> </li>
<li> <a href="/about">About</a> </li>
</ul>

However, those aren’t regular anchor elements. They change the URL without refreshing the page. Test it.

And now add a <a> element to the JSX code and test one more time:

<ul>
<li> <Link to="/">Home</Link> </li>
<li> <Link to="/messages">Messages</Link> </li>
<li> <Link to="/about">About</Link> </li>
<li>
    <a href="/messages">Messages (with a regular anchor element)</a> 
  </li>
</ul>

Do you notice the difference?

<Route>

Right now, the URL changes when a link is clicked, but not the UI. Let’s fix that.

I’m going to create three components for each route. First, src/component/Home.js for the route /:

import React from 'react';

const Home = () => (
  <div>
    <h2>Home</h2>
    My Home page!
  </div>
);

export default Home;

Then, src/component/Messages.js for the route /messages:

import React from 'react';

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

export default Messages;

And finally, src/component/About.js for the route /about:

import React from 'react';

const About = () => (
  <div>
    <h2>About</h2>
    This example shows how to use React Router!
  </div>
);

export default About;

To specify the URL that corresponds to each component, you use the <Route> in the following way:

<Route path="/" component={Home}/>
<Route path="/messages" component={Messages}/>
<Route path="/about" component={About}/>

With other router libraries (and even in previous versions of React Router), you have to define these routes in a special file, or at least, outside your application.

This doesn’t apply to React Router 4. These components can be placed anywhere inside of the router, and the associated component will be rendered in that place, just like any other component.

So in src/App.js, import all these components and add a section after the menu:

// …
import {
  Route,
  Link
} from 'react-router-dom'

import Home from './components/Home';
import About from './components/About';
import Messages from './components/Messages';

class App extends Component {
  render() {
    return (
      <div className="App">
        ...
        <div className="menu">
            ...
        </div>
        <div className="App-intro">
          <Route path="/" component={Home}/>
          <Route path="/messages" component={Messages}/>
          <Route path="/about" component={About}/>
        </div>
      </div>
    );
  }
}

In the browser, you should see something like this:

However, look what happen when you go to the other routes:

By default, routes are inclusive, more than one <Route> component can match the URL path and render at the same time.

Routes are the most important concept in React Router, and you need to know some things about them. Let’s talk about routes in the next section.

Understanding routes

The matching logic of the <Route> component is delegated to the path-to-regexp library. I encourage you to check all the options and modifiers of this library and test it live with the express-route-tester.

In the previous example, since the /message and /about paths also contain the character /, they are also matched and rendered.

With this behavior, you can display different components just by declaring that they belong to the same (or similar) path.

There’s more than one solution.

The first one uses the exact property to render the component only if the defined path matches the URL path exactly:

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

If you test the application, you’ll see that everything works fine.

The routes /message and /about are still evaluated, but they are not an exact match for / now.

However, if we know that only one route will be chosen, we can use a <Switch> component to render only the first route that matches the location:

// …
import {
  Route,
  Link,
  Switch
} from 'react-router-dom'
// …
class App extends Component {
  render() {
    return (
      …
      <div className="App-intro">
          <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/messages" component={Messages} />
            <Route path="/about" component={About} />
          </Switch>
        </div>
      </div>
    );
  }
}

<Switch> will make the path matching exclusive rather than inclusive (as if you were using <Route> components).

For example, even if you duplicate the route for the Messages component:

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/messages" component={Messages} />
  <Route path="/messages" component={Messages} />
  <Route path="/about" component={About} />
</Switch>

When visiting the /messages path, the Messages component will be rendered only once.

However, notice that you’ll still need to specify the exact property for the / path, otherwise, /message and /about will also match /, and the Home component will always be rendered (since this is the first route matched):

But what happens when a non-existent path is entered? For example http://localhost:3000/non-existent:

In a regular JavaScript switch statement, you’ll specify a default clause for this case, right?

In a <Switch> component, this default behavior can be implemented with a <Redirect> component:

// …
import {
  Route,
  Link,
  Switch,
  Redirect
} from 'react-router-dom'
// …
class App extends Component {
  render() {
    return (
      …
      <div className="App-intro">
          <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/messages" component={Messages} />
            <Route path="/about" component={About} />
            <Redirect to="/" />
          </Switch>
        </div>
      </div>
    );
  }
}

This component will navigate to a new location overriding the current one in the history stack:

Now, let’s cover something a little more advanced, nested routes.

Nested routes

A nested route is something like /about/react.

Let’s say that for the messages section, we want to display a list of messages. Each one in the form of a link like /messages/1/messages/2, and so on, that will lead you to a detail page.

You can start by modifying the Messages component to generate links for five sample messages in this way:

import React from 'react';

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

const Messages = () => (
  <div>
    <ul>
    {
        [...Array(5).keys()].map(n => {
            return <li key={n}>
                    <Link to={`messages/${n+1}`}>
                      Message {n+1}
                    </Link>
                  </li>;
        })
    }
    </ul>
  </div>
);

export default Messages;

This should be displayed in the browser:

To understand how to implement this, you need to know that when a component is rendered by the router, three properties are passed as parameters:

For our purposes, we’ll be using the match parameter.

When there’s a match between the router’s path and the URL location, a match object is created with information about the URL and the path. Here are the properties of this object:

  • params: Key/value pairs parsed from the URL corresponding to the parameters
  • isExacttrue if the entire URL was matched (no trailing characters)
  • path: The path pattern used to match
  • url: The matched portion of the URL

This way, in the Messages component, we can destructure the properties object to use the match object:

const Messages = ({ match }) => (
<div>
...
</div>
)

Replace /messages with the matched URL of the match object:

const Messages = ({ match }) => (
  <div>
    <ul>
    {
        [...Array(5).keys()].map(n => {
            return <li key={n}>
                    <Link to={`${match.url}/${n+1}`}>
                      Message {n+1}
                    </Link>
                  </li>;
        })
    }
    </ul>
  </div>
);

This way you’re covered if the path ever changes.

And after the message list, declare a <Route> component with a parameter to capture the message identifier:

import Message from './Message';
//…

const Messages = ({ match }) => (
  <div>
    <ul>
       ...
    </ul>
    <Route path={`${match.url}/:id`} component={Message} />
  </div>
);

In addition, you can enforce a numerical ID in this way:

<Route path={`${match.url}/:id(\\d+)`} component={Message} />

If there’s a match, the Message component will be rendered. Here’s its definition:

import React from 'react';

const Message = ({ match }) => (
  <h3>Message with ID {match.params.id}</h3>
);

export default Message;

In this component, the ID of the message is displayed. Notice how the ID is extracted from the match.params object using the same name that it’s defined in the path.

If you open the browser, you should see something similar to the following:

But notice that for the initial page of the messages section (/messages) or if you enter in the URL and invalid identifier (like /messages/a), nothing is printed under the list. A message would be nice, don’t you think?

You can add another route for this case, but instead of creating another component to just display a message, we can use the render property of the <Route> component:

<Route
  path={match.url}
  render={() => <h3>Please select a message</h3>
/>

You can define what is rendered by using one of the following properties of <Route>:

  • component: To render a component.
  • render: A function that returns the element or component to be rendered.
  • children: A function that also returns the element or component to be rendered. However, the returned element is rendered regardless of whether the path is matched or not.

Finally, we can wrap the routes in a <Switch> component to guarantee that only one of the two is matched:

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

const Messages = ({ match }) => (
  <div>
    <ul>
     ...
    </ul>
    <Switch>
      <Route path={`${match.url}/:id(\\d+)`} component={Message} />
      <Route
        path={match.url}
        render={() => <h3>Please select a message</h3>}
      />
    </Switch>
  </div>
);

However, you have to be careful. If you declare route that renders the message first:

 <Switch>
      <Route
        path={match.url}
        render={() => <h3>Please select a message</h3>}
      />
      <Route path={`${match.url}/:id(\\d+)`} component={Message} />
</Switch>

The Message component will never be rendered because a path like /messages/1 will match the path /messages.

If you declare the routes in this order, add exact to avoid this behavior:

<Switch>
    <Route
      exact
      path={match.url}
      render={() => <h3>Please select a message</h3>}
    />
    <Route path={`${match.url}/:id(\\d+)`} component={Message} />
</Switch>

Conclusion

In a few words, a router keeps your application UI and the URL in sync.

React Router is the most popular router library for React, and since version 4, React Router declarative defines routes with components, in the same style than React.

In this post, you have learned how to set up React Router, its most important components, how routes work, and how to build dynamic nested routes with path parameters.

But there’s still a lot of more to learn. For example, there a <NavLink>component that is a special version of the <Link> component that adds the properties activeClassName and activeStyle to give you styling options when the link matches the location URL.

The official documentation covers some basic as well as more advanced, interactive examples. Also, don’t forget to check out the advanced React Router concepts post, here on LogRocket’s blog.

Plug: LogRocket, a DVR for web apps

LogRocket Dashboard Free Trial Banner
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.

Esteban Herrera Family man, #Java and #Javascript developer. #Swift, and #VR/#AR hobbyist. Like #books, #movies and still trying many things. eherrera.net

One Reply to “React Router DOM: Setup, essential components, and parameterized routes”

Leave a Reply