React Router v5
Learning Goals:
- Understand and articulate the need for routing
- Be able to confidently implement React Router in a project
- Utilize URL params to build dynamic routes
- Understand how to test components using React Router
Vocab
BrowserRouter
A <Router> that uses the HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URLRouter
The class that <BrowserRouter> is extended fromRoute
Its most basic responsibility is to render some UI when a location matches the route’s pathLink
Links provide declarative, accessible navigation around your applicationNavLink
A special version of the <Link> that will add styling attributes to the rendered element when it matches the current URL.Redirect
Rendering a <Redirect> will navigate to a new location. The new location will override the current location in the history stack, like server-side redirects (HTTP 3xx) do.Switch
Renders the first child <Route> or <Redirect> that matches the location. <Switch> is unique in that it renders a route exclusively (only one route wins).match
A match object contains information about how a <Route path> matched the URL.
Prework
Before the lesson, complete the prework
React Router
Review
Prework Review
In small groups, discuss the following questions:
- Why use Router?
- Describe the high-level process of setting up Router in a project (packages to install, basic component needed)
- Describe the following components:
- Route
- Redirect
- Link
- NavLink
- Switch
Why Routing?
Routing refers to keeping a webpage up to date with the current url, and vice-versa.
Most of the apps you’ve written so far have been single-page applications. One HTML page whose content is updated through user interactions and JS. These DO NOT use routing.They work fine, but put some limits on the user experience of our applications.
What are some advantages routing can provide?
- Users can use urls to bookmark pages
- Users can use the back or forward button
- Users can easily share content from a page in the app
If you have written a multi-page application, you may have wrestled with Webpack configs in order to get all your pages built successfully.
Fortunately, routing with React is easy! We just need to use a library called React Router.
React Router allows us to conditionally render components based on the current url
The Code
Rather than tell you about how Router works, we’ll work through a series of exercises and examples.
We’ll be using this repo to solve a series of challenges listed below.
Installation Instructions
git clone https://github.com/turingschool-examples/react-router-v5
cd react-router-v5
npm i
npm start
# open your text editor
Code Sand Box Template
You’re also welcome to use the code sand box template, found here.
The App is not fully put together. It has a series of components that will serve as building blocks of the final component. You won’t be building out new components, but you will be editing existing ones.
Setting up Router
Before we break out into groups, we’ll review how to set up Router as a class.
Look through the codebase
Get oriented with the application. Check out all the components, try and write a short summary of what each is doing.
The <Home />
component is rendering a welcome message. Right now, nothing but a nav bar is being rendered by the App. Let’s use router to render the <Home />
component as a landing page.
Remember that React Router conditionally renders components based on the current url. So our goal is to render the
Setting up Router
To use React Router, we need to wrap any components that will use a React Router-provided-component in some kind of Router component.
We’ll use a Browser Router, since our app will be used in the browser. This Router provides access to the HTML5 History API. But we won’t worry abou those details just yet.
Hint
We’ll come back to this later in the lesson…
The first step is installing react router:
npm install react-router-dom
Once you have React Router installed, import your chosen Router.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App/App';
import { BrowserRouter } from 'react-router-dom';
const router = <BrowserRouter> <App /> </BrowserRouter>;
ReactDOM.render(router, document.getElementById('root'));
Finally, add a Route for the Home
component into your App
import React, { Component } from 'react';
import './App.css';
import puppies from '../data/puppy-data.js';
import sharks from '../data/shark-data.js';
import Creatures from '../Creatures/Creatures';
import Home from '../Home/Home';
import CreatureDetails from '../Creatures/CreatureDetails';
import { Route } from 'react-router-dom';
export default class App extends Component {
render() {
return (
<main className="App">
<nav>
<a href="/puppies" className="nav">Puppies</a>
<a href="/sharks" className="nav">Sharks</a>
</nav>
<h1>Puppies or Sharks?</h1>
<Route path="/" component={ Home }/>
</main>
);
}
}
We picked /
for the path in the route becuase it designates that there won’t be anything after the URL’s domain name. This represents the base url.
Exercise # 1: Render Puppies
Your goal is click on the word Puppies and see a grid of 9 puppies on the DOM. The page should look something like the picture on the lesson plan. While you may change components as needed, you shouldn’t outright delete content from the page to achieve this.
Take 10 minutes in pairs to get the puppies rendering
Hints:
- Use the Creatures component. Formatting and styling is handled for you.
- What additional react-router components should you use? Do any current components need to change?
- How do you pass props into a component rendered by a
<Route />
?
Solution
/ App.js
import './App.css';
import puppies from '../data/puppy-data.js';
import sharks from '../data/shark-data.js';
import Creatures from '../Creatures/Creatures';
import Home from '../Home/Home';
import { Route, NavLink } from 'react-router-dom';
export default class App extends Component {
render() {
return (
<main className="App">
<nav>
<NavLink to="/puppies" className="nav">Puppies</NavLink>
<NavLink to="/sharks" className="nav">Sharks</NavLink>
</nav>
<h1>Puppies or Sharks?</h1>
<Route path="/" component={ Home }/>
<Route path="/puppies" render={() => <Creatures name="puppies" data={puppies} />} />
</main>
);
}
}
Render exact
matches
Check out what happens when you take the exact
prop out of one of your Routes. Why do you think the Home
page is rendering at /puppies
?
The exact
prop can be used to make sure that partial matches of a URL don't trigger a render.
Render Methods
According to the docs, Routes have three possible methods for rendering a component on match:
component
render
children
We recommend to use render
or children
– they work more efficiently when re-rendering components. We’ll take a look at some more benefits they provide after the next exercise.
Here’s an example of their syntax: Component
<Route path='/unicorns' component={ Unicorns } />
Render
<Route path='/unicorns' render={ () => <Unicorns /> }
This also allows you to define and pass specific properties to a component dynamically. For example:
<Route path='/ideas/:id' render={({ match }) => {
const idea = ideas.find(idea => idea.id === parseInt(match.params.id));
if (!idea) {
return (<div>This idea does not exist! </div>);
}
return <ListItem match={match} {...idea} />
}} />
Children
<Route path='/other-unicorns' children={ () => <Unicorns /> } />
Exercise # 2: Rendering Sharks
Get the sharks link working as well!
Solution
// App.js
import './App.css';
import puppies from '../data/puppy-data.js';
import sharks from '../data/shark-data.js';
import Creatures from '../Creatures/Creatures';
import Home from '../Home/Home';
import { Route, NavLink } from 'react-router-dom';
export default class App extends Component {
render() {
return (
<main className="App">
<nav>
<NavLink to="/puppies" className="nav">Puppies</NavLink>
<NavLink to="/sharks" className="nav">Sharks</NavLink>
</nav>
<h1>Puppies or Sharks?</h1>
<Route exact path="/" render={ Home }/>
<Route path="/puppies" render={() => <Creatures name="puppies" data={puppies} />} />
<Route path="/sharks" render={() => <Creatures name="sharks" data={sharks} />} />
</main>
);
}
}
Route Props
Let’s take a close look at what happens when a Route renders.
Route render methods all provide access to route props, either automatically to the component they render, or via the callback function that the methods take.
These props include:
<Route path='/unicorns' render={ ({ history, location, match }) => <Unicorns /> }
history and location are worth looking into on your own, but today we’ll focus on match
.
The match
gives us information about how and why the application matched. And it allows us to do some pretty cool stuff.
match.params
The params
property of the match prop gives us an object with key value pairs of dynamic url parameters, and any strings that match them.
For instance, we could make our routes for animals more dynamic by doing this:
<Route
exact
path="/:animal"
render={({ match }) => {
const whichAnimal = match.params.animal === 'sharks' ? sharks : puppies
return <Creatures name={`I love ${match.params.animal}`} data={whichAnimal} />
}}
/>
and then navigate to either /puppies
or /sharks
, we can see that the <Creatures />
component is rendering the correct data based on the params
from the url.
params
allows us to define shapes of a url that will cause a match, then access the data from that url in our components.
This can be great for dynamically rendering content based on things in the url, like an id. Let’s do that!
Exercise #3: Dynamic Routing
Take a look at the CreatureDetails Component. It takes in all data for a given creature, and displays it on the page.
Your Task is to make a route that will dynamically render a CreatureDetails component for a puppy based on its ID
Hints:
- Use the CreatureDetails component
- For example the URL
/puppies/1
should render a view just for the puppy with an ID of 1 in the dataset - How can you find a one puppy’s data in an array based on its id?
Solution
The new route could look something like this:
// App.js
<Route
exact
path="/puppies/:id"
render={({match}) => {
const { id } = match.params;
const creatureToRender = puppies.find(creature => creature.id === parseInt(id));
return <CreatureDetails {...creatureToRender} />
}}
/>
Exercise #4: Unit Testing the App
Uncomment the code blocks inside of the Unit Test portion of App.test.js
Take 10 minutes and figure out why the tests are failing. Using the docs, try and make them pass
Hint:
- Think about which libraries this problem deals with. Google carefully…
Solution
We need to wrap the App inside of a Router! In the browser, this is handled at the entry point (index.js), but in the tests we’re rendering the App on its own.
We can do one of two things depending on which docs you use:
- Create a custom history object and pass it to a Router (NOT a BrowserRouter)
- Use a Memory Router
Let’s look at both of these solutions.
Option 1: Creating a new history object
For this solution, we have to look into the history package from React Training.
Routers use this package to manage their session history. BrowserRouter creates a history object under the hood, which gets its current location from the browser (jsdom in the case of jest’s testing environment), and updates the browser history via a few methods that you can read more about if you’re interested.
We can also create our own history object.
However, BrowserRouter CANNOT receive a custom history object, so if we want to make an manipulate our own history, we’ll have to wrap our component in a Router. So we can use that history object to overwrite the Router’s default history. This allows us to instantiate a new Router at a specific location (url) of our choosing.
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
// Make a new blank history object
const customHistory = createMemoryHistory();
// Overwrite the history of the Router:
const routerWithCustomHistory = <Router history={customHistory}></Router>
So our final solution to the Unit Test could look like this:
import React from 'react';
import App from './App';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { createMemoryHistory } from 'history'
import { Router } from 'react-router-dom'
describe('App', () => {
describe('Unit Tests', () => {
it('Should render the heading', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<App />
</Router>
);
const heading = screen.getByRole('heading', { name: 'Puppies or Sharks?' })
expect(heading).toBeInTheDocument();
});
it('Should render a nav', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<App />
</Router>
);
const navigation = screen.getByRole('navigation');
expect(navigation).toBeInTheDocument();
});
});
});
Note This techniques comes from React Testing Library
Option 2: Using a MemoryRouter
Alternatively, we can use a different kind of Router that doesn’t get information about history from the DOM.
MemoryRouter doesn’t read or write to the address bar. As such, it’s useful for tests and non-browser environments like React Native.
MemoryRouter WILL NOT WORK IN YOUR BROWSER CODE
// App.test.js
import React from "react";
import App from "./App";
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { MemoryRouter } from "react-router-dom";
describe("App", () => {
describe("Unit Tests", () => {
it("Should render the heading", () => {
render(
<MemoryRouter>
<App />
</MemoryRouter>
);
const heading = screen.getByRole("heading", { name: "Puppies or Sharks" });
expect(heading).toBeInTheDocument();
});
it("Should render a nav", () => {
render(
<MemoryRouter>
<App />
</MemoryRouter>
);
const navigation = screen.getByRole("navigation");
expect(navigation).toBeInTheDocument();
});
});
});
Exercise #5: Integration Testing the App
Uncomment the code in the Integration Test portion of App.test.js
.
Take 10 minutes and answer the following:
- What are the tests trying to do?
- Try and get to the bottom of the error
Hint:
- Once again, flex those Googling muscles. What issue are you seeing? Use what you learned from the unit tests to help you with your solution.
Solution
Once again, we have two options based on whether we pass it to Router
and use a custom history object or we use Memory Router
.
Option 1: Creating a new history object
import React from 'react';
import App from './App';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
describe('App', () => {
describe('Integration Tests', () => {
it('Should render 9 puppies on click', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<App />
</Router>
);
const puppiesLink = screen.getByRole('link', { name: 'Puppies'});
const welcomeMessage = screen.getByRole('heading', {name: 'Welcome! Click on the links above to see a variety of creatures'});
expect(welcomeMessage).toBeInTheDocument();
fireEvent.click(puppiesLink);
expect(welcomeMessage).not.toBeInTheDocument();
const images = screen.getAllByRole('img');
expect(images).toHaveLength(9);
});
it('Should render 9 sharks on click', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<App />
</Router>
);
const sharksLink = screen.getByRole('link', { name: 'Sharks'});
const welcomeMessage = screen.getByRole('heading', {name: 'Welcome! Click on the links above to see a variety of creatures'});
expect(welcomeMessage).toBeInTheDocument();
fireEvent.click(sharksLink);
expect(welcomeMessage).not.toBeInTheDocument();
const images = getAllByRole('img');
expect(images).toHaveLength(9);
});
});
});
Option 2: Using a MemoryRouter
import React from 'react';
import App from './App';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom';
describe('App', () => {
describe('Integration Tests', () => {
it('Should render 9 puppies on click', () => {
render(<MemoryRouter><App /></MemoryRouter>);
const puppiesLink = screen.getByRole('link', { name: 'Puppies'});
const welcomeMessage = screen.getByRole('heading', {name: 'Welcome! Click on the links above to see a variety of creatures'});
expect(welcomeMessage).toBeInTheDocument();
userEvent.click(puppiesLink);
expect(welcomeMessage).not.toBeInTheDocument();
const images = screen.getAllByRole('img');
expect(images).toHaveLength(9);
});
it('Should render 9 sharks on click', () => {
render(<MemoryRouter><App /></MemoryRouter>);
const sharksLink = screen.getByRole('link', { name: 'Sharks'});
const welcomeMessage = screen.getByRole('heading', {name: 'Welcome! Click on the links above to see a variety of creatures'});
expect(welcomeMessage).toBeInTheDocument();
userEvent.click(sharksLink);
expect(welcomeMessage).not.toBeInTheDocument();
const images = screen.getAllByRole('img');
expect(images).toHaveLength(9);
});
});
});
Extra Resources:
Tutorials / Guides:
- React Training’s 13 minute overview of React Router
- The Hitchhiker’s Guide to React Router - learn Router in 20 minutes
- The Hitchhiker’s Guide to React Router - match, location, history
Helpful Articles / Docs:
- Routing and Form Submission
- Old lesson plan
- React Router Testing Recipe from RTL
- Memory Router docs
- history package docs
Check out this additional information on some Router Components:
Link
Provides declarative, accessible navigation around your application.
Things to know:
- Link can contain an open and closing tag or be a self-closing tag
- Link takes a
to
attribute as well as an optionalreplace
attribute to
tells the app which path to redirect to. This can be a string or an objectreplace
is a boolean that whentrue
will replace the current entry in the history stack instead of adding a new one
<Link to='/unicorns' />
<Link to='/unicorns'> Unicorns </Link>
NavLink
A special version of the <Link>
that will add styling attributes to the rendered element when it matches the current URL.
It can take the following attributes:
- activeClassName: string - defaults to
active
- activeStyle: object
- exact: bool
- strict: bool
- isActive: func
- location: object
Read about each of these here
<NavLink to='/about'>About</NavLink>
Redirect
Rendering a <Redirect>
will navigate to a new location. The new location will override the current location in the history stack, like server-side redirects (HTTP 3xx) do.
More of a nice to know for now. This is something that can be used if the user does something wrong. ie. went to a route they don’t have permissions to access.
It can take the following attributes:
- to: string
- to: object
- push: bool
- from: string
<Redirect to='/not/unicorns' />
Switch
Renders the first child <Route>
or <Redirect>
that matches the location. <Switch>
is unique in that it renders a route exclusively (only one route wins). In contrast, every <Route>
that matches the location renders inclusively (more than one route can match and render at a time)
<Switch>
<Route exact path='/' component={Home} />
<Route path='/users/add' component={UserAddPage} />
<Route path='/users' component={UsersPage} />
<Redirect to='/' />
</Switch>
The docs do a great job of quickly showing what Switch is all about.