React Router Patterns
March 01, 2020
The goals of this lecture are to:
- Describe how router URL parameters work
- Understand
Switch
and when to use it - Compare different ways to redirect using React Router
- Learn how to test React Router components
Routing in React
As we begin to build more complex React applications, the number of routes that we'll need will continue to grow.
We could, of course, hard-code our routes:
function App() {
return (
<App>
<Route path="/food/tacos">
<Food name="tacos" />
</Route>
<Route path="/food/salad">
<Food name="salad" />
</Route>
<Route path="/food/sushi">
<Food name="sushi" />
</Route>
</App>
);
}
But we won't because this would be unmanageable.
Not only is there a significant amount of duplication, but any time we'd like to add more foods, we'd have to update our route with yet another addition.
Instead, we're going to use URL parameters.
Routing with URL Parameters
Just like we've seen in our work with both Flask and Express, we will denote URL parameters in our routes using the :
character, followed by the parameter name as seen on Line 11:
import React from "react";
import Nav from "./Nav";
import { Route, BrowserRouter } from "react-router-dom";
import Food from "./Food";
function App() {
return (
<div className="App">
<BrowserRouter>
<Nav />
<Route path="/food/:name">
<Food />
</Route>
</BrowserRouter>
</div>
);
}
export default App;
Accessing URL Parameters
Once a parameter has been placed in our route, we can access the value of it using the useParams
hook.
useParams
will be an object with multiple keys, each one corresponding to a parameter in the route.
In our component, after importing useParams
, we invoke the hook and destructure the name of the parameters we'd like to grab (Line 7), as we see below with name
:
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
const GIPHY_URL = "http://api.giphy.com/v1";
function Food() {
const { name } = useParams();
const [src, setSrc] = useState(null);
useEffect(() => {
async function fetchGif(searchTerm) {
let res = await axios.get(`${GIPHY_URL}/gifs/search`, {
params: { q: searchTerm, api_key: "dc6zaTOxFJmzC" }
});
setSrc(res.data.data[0].images.original.url);
}
fetchGif(name);
}, [name]);
let img = src ? <img src={src} alt={name} /> : null;
return (
<div>
<h1>Here's a pic of {name}.</h1>
{img}
</div>
);
}
If it's the case that our route features multiple parameters, we just grab those additional keys in the same way:
<Route path="/food/:foodName/drink/:drinkName">
<Food />
</Route>
And in our component:
const { foodName, drinkName } = useParams();
Inclusively Matching Routes
By default, routes match inclusively.
What this means is that if multiple routes match the same pattern, each matching route will render and be displayed.
We do have the ability to use the exact
attribute, but it's easy to make mistakes with routing logic.
Take a look at the following example:
function Routes() {
return (
<div>
<Route exact path="/about">
<About />
</Route>
<Route exact path="/contact">
<Contact />
</Route>
<Route exact path="/blog/:slug">
<Post />
</Route>
<Route exact path="/blog">
<BlogHome />
</Route>
<Route exact path="/">
<Home />
</Route>
</div>
);
}
How many routes will match about
? How about blog
? And /blog/unicorns-ftw
?
Imagine with a larger application. It can get confusing and hard to manage quite quickly!
The Switch Component
In terms of routing, it's often easier to read and understand routes when they are exclusive (meaning that the first match that's found is displayed), rather than inclusive (find and display all route matches).
Rather than surrounding our routes with a simple div
, we'll introduce exclusive routing and wrap our routes with the <Switch />
component:
function Routes() {
return (
<Switch>
<Route exact path="/about">
<About />
</Route>
<Route exact path="/contact">
<Contact />
</Route>
<Route exact path="/blog/:slug">
<Post />
</Route>
<Route exact path="/blog">
<BlogHome />
</Route>
<Route exact path="/">
<Home />
</Route>
</Switch>
);
}
Switch
will find the first Route that matches and renders only that.
Handling 404
In the case that we'd like to display to the user a particular component when the route they're trying to access doesn't exist, we'd add it as our last route:
function Routes() {
return (
<Switch>
// .. other routes
<Route>
<NotFound />
</Route>
</Switch>
);
}
Redirects
Using React Router, we can mimic the behavior of server-side redirects, like when a user submits a form and we'd like to take them to a different page.
In addition, we can also use redircts as an alternative to using a catch-all 404 component for a user accessing a route that doesn't exist.
Using React Router, there are two ways to redirect:
- Using the
component . Useful for “you shouldn’t have gotten here, go here instead” - Calling .push method on history object. Useful for “you finished this, now go here”
Redirect Component
To use the <Redirect>
component:
function Routes() {
return (
<Switch>
<Route exact path="/about"><About /></Route>
<Route exact path="/contact"><Contact /></Route>
<Route exact path="/blog/:slug"><Post /></Route>
<Route exact path="/blog"><BlogHome /></Route>
<Route exact path="/"><Home /></Route>
<Redirect to="/" />
</Switch>
);
}
The history Object
When we invoke the useHistory
hook, we are returned an object that is a wrapper over the browser's history API.
Aside from various properties, we also get access to a .push(url)
method, which adds a URL to the session's history.
By adding a URL to the history, if the user hits the back button, they will return to the URL provided.
In the following example, we've created a handleSubmit
method that, when the user submits a form, they will be redirected to another page entirely:
function Contact() {
const [email, setEmail] = useState("");
const history = useHistory();
function handleChange(e) {
setEmail(e.target.value);
}
function storeEmail() {
console.log("jk, no email storage");
}
function handleSubmit(evt) {
evt.preventDefault();
storeEmail(email);
// imperatively redirect to homepage
history.push("/");
}
}
React Router Tips
As we move forward with our work using React Router, keep in mind the following:
- Favor Route child components over other options
- Keep routes up high in the component hierarchy
- Use Switch!
- Avoid nested routes
- Use
history.push()
in response to user actions (like submitting a form)
Testing React Router
Components that are rendered by the router are harder to test than regular components for a couple of reasons.
First, components may depend on specific router hooks that we’ll have to mock, such as useParams
.
What's more, components require the context of a parent router during testing.
Mocking Router Context
Let's take a look at an example.
Consider a Nav
component:
function Nav() {
return (
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About Us</Link></li>
<li><Link to="/contact">Contact</Link></li>
<li><Link to="/blog">Blog</Link></li>
<li><Link to="/blargh">Broken Link</Link></li>
</ul>
);
}
When we try to render this component within our tests, as seen below:
it('renders without crashing', function() {
render(<Nav />);
});
...we end up with an error about the context router
.
To avoid this error, which will inform us that we shouldn't use <Link>
outside of a <Router>
,
we'll use a mock router.
The mock router that we'll use, MemoryRouter, is designed specifically for tests:
import { MemoryRouter } from 'react-router-dom';
it('mounts without crashing', function() {
const { getByText } = render(
<MemoryRouter>
<Nav />
</MemoryRouter>
);
const blogLink = getByText(/Blog/i);
expect(blogLink).toBeInTheDocument();
});