Skip to main content

Routing

Overview

  • Similar in function to a server-side router in an MVC framework
    • Associates a route (url) with a particular controller action
  • React Router switches between (page/container) components when a route changes
  • Back button is broken by default when page/container components change
    • the browser's history is not updated with a new url when page/container components change
    • React Router programmatically adds entries to the browser's history
    • enables the back button to work in React applications

There are two versions:

  1. BrowserRouter (react-router-dom) for web applications.
  2. NativeRouter (react-router-native) for use with React Native.

Installation

  1. Install the package

    npm install react-router-dom
  2. Add the script tag

    //index.html

    <script src="/node_modules/react/umd/react.development.js"></script>
    <script src="/node_modules/react-dom/umd/react-dom.development.js"></script>
    + <script src="/node_modules/react-router-dom/umd/react-router-dom.js"></script>
    <script src="/node_modules/@babel/standalone/babel.min.js"></script>
    <script src="/node_modules/axios/dist/axios.min.js"></script>
    <script type="text/babel" src="/main.js"></script>

    ! Be sure that the main.js script tag's src attribute starts with a / or the router will not work properly when you refresh the page.

  3. Log the RouterRouterDOM to verify it is installed properly

    • Also export the common components so they are easier to use
    //main.js
    console.log(window.ReactRouterDOM);

    const {
    BrowserRouter: Router,
    Route,
    Link,
    Prompt,
    Switch,
    Redirect,
    NavLink,
    } = window.ReactRouterDOM;
  4. In the console you should see:

    {BrowserRouter: ƒ, HashRouter: ƒ, Link: ƒ, NavLink: ƒ, MemoryRouter: ƒ, …}

Basics

  1. Add these styles
/* styles.css */

.container {
border: 1px solid #ddd;
margin: 30px;
padding: 30px;
}

nav ul {
list-style: none;
}

nav ul li {
display: inline;
}

nav ul li:after {
content: ' | ';
}

nav ul li:last-child:after {
content: '';
}

.active {
background-color: #bee5eb;
}
  1. Try this code
const {
BrowserRouter: Router,
Route,
Link,
Prompt,
Switch,
Redirect,
NavLink,
} = window.ReactRouterDOM;

function Home() {
return <h2>Home</h2>;
}

function About() {
return <h2>About</h2>;
}

function Contact() {
return <h2>Contact</h2>;
}

function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about/">About</Link>
</li>
<li>
<Link to="/contact/">Contact</Link>
</li>
</ul>
</nav>

<div className="container">
<Route path="/" exact component={Home} />
<Route path="/about/" component={About} />
<Route path="/contact/" component={Contact} />
</div>
</div>
</Router>
);
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  1. Change the Link tags to NavLink tags
<nav>
<ul>
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/about/">About</NavLink>
</li>
<li>
<NavLink to="/contact/">Contact</NavLink>
</li>
</ul>
</nav>
  1. Add the following style
/* styles.css */

.active {
background-color: #bee5eb;
}

You can change the name of the class used for the active item by setting activeClassName. See the documentation on activeClassName for more information.

  1. Refresh the browser and see the navigation items are highlighted.
  2. Notice that Home is always highlighted because the \ path is the start of the other paths \about and \contact
  3. Add an exact attribute to the Home NavLink to get this working as intended.
<nav>
<ul>
<li>
<NavLink exact to="/">
Home
</NavLink>
</li>
<li>
<NavLink to="/about">About</NavLink>
</li>
<li>
<NavLink to="/contact">Contact</NavLink>
</li>
</ul>
</nav>

For more information see the API documentation for \<NavLink>

Not Found (404)

  1. Be sure you are running a development web server like serve with the -s flag.

package.json

"scripts": {
"start": "serve -s",
...
},
...
  1. Change the URL to http://localhost:5000/noroute

  2. The navigation renders but the page is blank. Ideally, we would like to show a NotFound component when this happens.

    To achieve this we need to understand two things:

    • A <Route> with no path will always render a component
    • A <Switch> component will display the first matching route listed inside of it

3) Create a NotFound component

function NotFound() {
return (
<>
<h2>Uh oh.</h2>
<p>
The page you requested could not be found. Is there any chance you were
looking for one of these?
</p>
</>
);
}
  1. Add a route for it with no path
    <Route path="/" exact  component={Home} />
<Route path="/about/" component={About} />
<Route path="/contact/" component={Contact} />
+ <Route component={NotFound} />
  1. Navigate to the home route and then the contact route and notice the NotFound component shows when visiting every route
  2. Wrap the list of routes in a Switch component
+ <Switch>
<Route path="/" exact component={Home} />
<Route path="/about/" component={About} />
<Route path="/contact/" component={Contact} />
<Route component={NotFound} />
+ </Switch>
  1. Navigate to the various routes again and notice that only when you manually go to a route that doesn't exist like: /noroute the NotFound component renders.
  • The attribute exact on a <Route> controls what is displayed into the page.
  • The attribute exact on a <NavLink> controls what is active (highlighed) in the navigation.

Parameters

URL Parameters

This example builds on the code from the previous demonstrations in this section.

  1. Create a Movie model class.
  • Add it just before the App component
class Movie {
constructor(id, name, description, type) {
this.id = id;
this.name = name;
this.description = description;
this.type = type;
}
}
  1. Create an array of mock movies
const movies = [
new Movie(
1,
' Titanic',
'A seventeen-year-old aristocrat falls in love with a kind but poor artist aboard the luxurious, ill-fated R.M.S. Titanic.',
'Drama'
),
new Movie(
2,
' E.T. the Extra-Terrestrial',
'A troubled child summons the courage to help a friendly alien escape Earth and return to his home world.',
'Fantasy'
),
new Movie(
3,
'The Wizard of Oz',
// tslint:disable-next-line:max-line-length
'Dorothy Gale is swept away from a farm in Kansas to a magical land of Oz in a tornado and embarks on a quest with her new friends to see the Wizard who can help her return home in Kansas and help her friends as well.',
'Fantasy'
),
new Movie(
4,
'Star Wars: Episode IV - A New Hope ',
// tslint:disable-next-line:max-line-length
'Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee and two droids to save the galaxy from the Empire/`s world-destroying battle-station while also attempting to rescue Princess Leia from the evil Darth Vader.',
'Action'
),
];
  1. Create a Movies component to list movies
function MoviesList(props) {
const movieListItems = props.movies.map((movie) => (
<li key={movie.id}>
<Link to={`${props.match.url}/${movie.id}`}>{movie.name}</Link>
</li>
));
return (
<div>
<h2>Movies</h2>
<ul>{movieListItems}</ul>
</div>
);
}
  1. Add a Route to go to the Movies component

Notice how we pass props to a the Movies component which is rendered by the React Router

<Route
exact
path="/movies"
render={(props) => <Movies {...props} movies={movies} />}
/>
  1. Add a NavLink to navigate to the Movies component.
<ul>
...
<li>
<NavLink to="/contact/">Contact</NavLink>
</li>
+ <li>
+ <NavLink to="/movies">Movies</NavLink>
+ </li>
</ul>
  1. Create a MovieDetail component to show the detail about a particular movie.
function MovieDetail(props) {
const movieId = Number(props.match.params.id);
const movie = movies.find((movie) => movie.id === movieId);

return (
<div>
<h2>Movie Detail</h2>
<h3>{movie.name}</h3>
<p>{movie.description}</p>
</div>
);
}
  1. Add a Route to go to the MovieDetail component.
<div className="container">
<Route path="/" exact component={Home} />
<Route path="/about/" component={About} />
<Route path="/contact/" component={Contact} />

<Route exact
path="/movies"
render={props => <MoviesList {...props} movies={movies} />}
/>
+ <Route path={`/movies/:id`} component={MovieDetail} />
</div>

Notice how the params are automatically added to the props of the component being rendered by their route.

  1. To better understand how this is working temporarily add this line to the MovieDetail component.
function MovieDetail(props) {
const path = props.match.path;
const movieId = Number(props.match.params.id);
const movie = movies.find(movie => movie.id === movieId);

return (
<div>
<h2>Movie Detail</h2>
<h3>{movie.name}</h3>
<p>{movie.description}</p>
+ <pre>{JSON.stringify(props, null, ' ')}</pre>
</div>
);
}
  1. Refresh the page and you will see all the information the Route automatically adds to the component.
{
"history": {
"length": 4,
"action": "PUSH",
"location": {
"pathname": "/movies/2",
"search": "",
"hash": "",
"key": "t7rwjz"
}
},
"location": {
"pathname": "/movies/2",
"search": "",
"hash": "",
"key": "t7rwjz"
},
"match": {
"path": "/movies/:id",
"url": "/movies/2",
"isExact": true,
"params": {
"id": "2"
}
}
}

Query Parameters

Modify the Movies component to filter by movie type (genre).

  1. Destructure the needed props in the function signature and rename movies to allMovies

  2. Parse the query string parameter(s)

    React Router does not have any opinions about how you parse URL query strings. Some applications use simple key=value query strings, but others embed arrays and objects in the query string. So it's up to you to parse the search string yourself.

    In modern browsers that support the URL API, you can instantiate a URLSearchParams object from location.search and use that.

    In browsers that do not support the URL API (read: IE) you can use a 3rd party library such as query-string.

  3. Create an empty movies array and then filter the movies if a type is passed

  4. Add links with search params for the various movie types

    function Movies({ movies: allMovies, location, match }) {
    let movies = [];
    let queryParams = new URLSearchParams(location.search);
    let type = queryParams.get('type');
    if (type) {
    movies = allMovies.filter((movie) => movie.type === type);
    } else {
    movies = allMovies;
    }
    const movieListItems = movies.map((movie) => (
    <li key={movie.id}>
    <Link to={`${match.url}/${movie.id}`}>{movie.name}</Link>
    </li>
    ));
    return (
    <div>
    <nav>
    <ul>
    <li>
    <Link to={{ pathname: '/movies', search: '?type=Drama' }}>
    Drama
    </Link>
    </li>
    <li>
    <Link to={{ pathname: '/movies', search: '?type=Fantasy' }}>
    Fantasy
    </Link>
    </li>
    <li>
    <Link to={{ pathname: '/movies', search: '?type=Action' }}>
    Action
    </Link>
    </li>
    </ul>
    </nav>
    <h2>Movies</h2>
    <ul>{movieListItems}</ul>
    </div>
    );
    }
  5. Bonus: If time permits, add the following code to highlight the secondary navigation movie types

function isLinkActive(currentType, linkType) {
return currentType === linkType ? 'active' : '';
}

function Movies({ movies: allMovies, location, match }) {
...
<nav>
<ul>
<li>
<Link
className={isLinkActive(type, 'Drama')}
to={{ pathname: '/movies', search: '?type=Drama' }}
>
Drama
</Link>
</li>
<li>
<Link
className={isLinkActive(type, 'Fantasy')}
to={{ pathname: '/movies', search: '?type=Fantasy' }}
>
Fantasy
</Link>
</li>
<li>
<Link
className={isLinkActive(type, 'Action')}
to={{ pathname: '/movies', search: '?type=Action' }}
>
Action
</Link>
</li>
</ul>
</nav>
}

Nesting

  1. Copy then remove the Movie detail Route from the App component
  2. Change the movies route to no longer require an exact match.
function App() {
return (
<Router>
...

<div className="container">
<Route path="/" exact component={Home} />
<Route path="/about/" component={About} />
<Route path="/contact/" component={Contact} />

<Route
- exact
path="/movies"
render={props => <Movies {...props} movies={movies} />}
/>
- <Route path={`/movies/:id`} component={MovieDetail} />
</div>
</Router>
);
}
  1. Paste the Movie detail Route into the Movies component (so it is a route nested inside another route)
function Movies({ movies: allMovies, location, match }) {
...
return (
...
<h2>Movies</h2>
<ul>{movieListItems}</ul>
+ <Route path={`/movies/:id`} component={MovieDetail} />
</div>
);
}
  1. Refresh the browser and notice that the movie detail now shows below the movie list after clicking a movie link.

React Router version 5

In React Router version 5 some hooks have been added which have significantly improved the API. The version 4 syntax is still supported (backwards compatible) when using version 5. Below is an example of how the example we have used throughout this section updated for version 5.

In summary:

  • Components can be nested inside a Route component instead of using the Component property
  • Hooks have been added including a useParams to make it easier to access parameters
const {
BrowserRouter: Router,
Route,
Link,
Prompt,
Switch,
Redirect,
NavLink,
useParams,
} = window.ReactRouterDOM;

function Home() {
return <h2>Home</h2>;
}

function About() {
return <h2>About</h2>;
}

function Contact() {
return <h2>Contact</h2>;
}

class Movie {
constructor(id, name, description, type) {
this.id = id;
this.name = name;
this.description = description;
this.type = type;
}
}

const movies = [
new Movie(
1,
' Titanic',
'A seventeen-year-old aristocrat falls in love with a kind but poor artist aboard the luxurious, ill-fated R.M.S. Titanic.',
'Drama'
),
new Movie(
2,
' E.T. the Extra-Terrestrial',
'A troubled child summons the courage to help a friendly alien escape Earth and return to his home world.',
'Fantasy'
),
new Movie(
3,
'The Wizard of Oz',
// tslint:disable-next-line:max-line-length
'Dorothy Gale is swept away from a farm in Kansas to a magical land of Oz in a tornado and embarks on a quest with her new friends to see the Wizard who can help her return home in Kansas and help her friends as well.',
'Fantasy'
),
new Movie(
4,
'Star Wars: Episode IV - A New Hope ',
// tslint:disable-next-line:max-line-length
'Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee and two droids to save the galaxy from the Empire/`s world-destroying battle-station while also attempting to rescue Princess Leia from the evil Darth Vader.',
'Action'
),
];

function MoviesList(props) {
const movieListItems = props.movies.map((movie) => (
<li key={movie.id}>
<Link to={`movies/${movie.id}`}>{movie.name}</Link>
</li>
));
return (
<div>
<h2>Movies</h2>
<ul>{movieListItems}</ul>
</div>
);
}

function MovieDetail() {
const { id } = useParams();
const movieId = Number(id);
const movie = movies.find((movie) => movie.id === movieId);

return (
<div>
<h2>Movie Detail</h2>
<h3>{movie.name}</h3>
<p>{movie.description}</p>
</div>
);
}

function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<NavLink exact to="/">
Home
</NavLink>
</li>
<li>
<NavLink to="/about">About</NavLink>
</li>
<li>
<NavLink to="/contact">Contact</NavLink>
</li>
<li>
<NavLink to="/movies">Movies</NavLink>
</li>
</ul>
</nav>

<div className="container">
<Route path="/" exact>
<Home />
</Route>
<Route path="/about/">
<About />
</Route>
<Route path="/contact/">
<Contact />
</Route>
<Route exact path="/movies">
<MoviesList movies={movies} />
</Route>

<Route path={`/movies/:id`}>
<MovieDetail />
</Route>
</div>
</div>
</Router>
);
}

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