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:
- BrowserRouter (react-router-dom) for web applications.
- NativeRouter (react-router-native) for use with React Native.
Installation
Install the package
npm install react-router-dom
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.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;In the console you should see:
{BrowserRouter: ƒ, HashRouter: ƒ, Link: ƒ, NavLink: ƒ, MemoryRouter: ƒ, …}
Basics
- 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;
}
- 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 />);
- 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>
- 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.
- Refresh the browser and see the navigation items are highlighted.
- Notice that
Home
is always highlighted because the\
path is the start of the other paths\about
and\contact
- 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)
- Be sure you are running a development web server like serve with the
-s
flag.
package.json
"scripts": {
"start": "serve -s",
...
},
...
Change the URL to
http://localhost:5000/noroute
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
- A
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>
</>
);
}
- 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} />
- Navigate to the home route and then the contact route and notice the
NotFound
component shows when visiting every route - 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>
- Navigate to the various routes again and notice that only when you manually go to a route that doesn't exist like:
/noroute
theNotFound
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.
- 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;
}
}
- 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'
),
];
- 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>
);
}
- 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} />}
/>
- 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>
- 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>
);
}
- 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.
- 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>
);
}
- 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).
Destructure the needed props in the function signature and rename movies to
allMovies
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.
Create an empty movies array and then filter the movies if a type is passed
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>
);
}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
- Copy then remove the
Movie
detail Route from theApp
component - 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>
);
}
- Paste the
Movie
detail Route into theMovies
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>
);
}
- 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 theComponent
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 />);