We have updated the content of our program. To access the current Software Engineering curriculum visit curriculum.turing.edu.
React Router 4
Learning Goals
- Be able to explain the need for routing
- Be able to implement React Router in a project
- Articulate what each of these components do:
- BrowserRouter
- Route
- Link / NavLink
- Redirect
- Switch
- Utilize url params to build dynamic routes
Vocab
BrowserRouter
A <Router> that uses the HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URLRoute
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.
React Router (v4)
Why Routing?
Up until now you’ve been creating single page applications that maybe have utilized conditional rendering based on a boolean in state. Something along the lines of…
class SomeComponent extends Component {
constructor() {
super();
this.state = {
showWelcome: false
}
}
render() {
return (
{ this.state.showWelcome && <Welcome /> }
)
}
}
This works…but as our applications grow larger and we need to render more components, these conditional toggles can become difficult to manage.
Suppose you have list of star wars characters and you only want to show a user’s favorites when they click on a Favorites
button…
You can easily accomplish this with React Router!
Enter React Router
From the Docs
React Router is a collection of navigational components that compose declaratively with your application. Whether you want to have bookmarkable URLs for your web app or a composable way to navigate in React Native, React Router works wherever React is rendering.
In a nutshell…
React Router allows us to:
- Define which component(s) are rendering based on the URL pathname (dynamic, nested views should have a URL of their own)
- Bookmark specific page/view within our application to reference at a later time
- Utilize the
back
andforward
buttons in our browser
In Your Notebook
What does this lead to? Why or how? - Take a minute to write your answer and then discuss with a partner.
Router
There are many high-level routers that come with the react-router-dom
module:
<BrowserRouter>
- for current browsers<HashRouter>
- for legacy browsers<MemoryRouter>
- for testing and non-browser environments like React Native<StaticRouter>
- for server side rendering
We will be focusing on BrowserRouter
which is A Router that uses the HTML5 history API to keep your UI in sync with the URL.
We will wrap our entire app with this router.
Ultimately it will allow our users to bookmark specific paths and utilize their forward/back buttons.
There are a few more tools we get with React Router that are important to know about:
Route
The Route component is a key piece of React Router. Its most basic responsibility is to render some UI when a location matches the route’s path.
The Route component expects a path
prop (string) that describes the path name.
There are 3 ways to render something with a Route:
<Route component>
<Route render>
<Route children>
Let’s say we have a Unicorns
component, here is what it would look like in each of these instances:
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} />
}} />
Render differs slightly from Component, let’s check out the docs to see what they say about it.
Children
<Route path='/other-unicorns' children={ () => <Unicorns /> } />
It works exactly like render except that it gets called whether there is a match or not.
Component > Render > Children
Component
supersedes Render
which supersedes Children
so be sure to only include one within a route.
Route Props
All three of these are rendered with route props, which include:
Important Note: If you have a component that is not rendered by a Route, but still needs access to the route props (match/location/history), you will need to use the withRouter
method provided by React Router. This will be necessary to make any of your React Router components (Link, Route, Redirect, Switch, etc.) work correctly. Take a few minutes and read the docs as well as this post for more info.
Component Research Spike (10 mins)
Take notes on the features of each of these components in your notebook!
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.
Time to Code!
Setup
Enough talk, let’s implement React Router!
- Clone this repo and
cd
into it - checkout the
in-class
branch npm i
npm start
- open your text editor
This application will provide us with a Main
landing page as well as 3 routes to pages containing:
- Unicorns
- Puppies
- Sharks
Additionally we will add a dynamic route to dig deeper into a specific creatures cards.
First let’s install react-router-dom
npm i react-router-dom
Next let’s go import it and wrap
//index.js
import { BrowserRouter } from 'react-router-dom'
const router = (
<BrowserRouter>
<App />
</BrowserRouter>
)
ReactDOM.render(router, document.getElementById('root'));
Next we will go to our App.js
file and begin constructing the routes for our application. We’ll need to import some additional pieces from the library.
//App.js
import { Route, NavLink } from 'react-router-dom'
Creating a Header Component
Now let’s build a header to persist on all views. We will use NavLink
so we can take advantage of the active
class.
Note
It comes with a default class of .active
so we can either use that without defining it, or define a new name using activeClassName
.
export default class App extends Component {
render() {
return (
<main className='App'>
<header>
<NavLink to='/unicorns' className='nav'> Unicorns </NavLink>
<NavLink to='/puppies' className='nav'> Puppies </NavLink>
<NavLink to='/sharks' className='nav'> Sharks </NavLink>
</header>
</main>
);
}
}
If you click on these links now, you should see the URL change to the routes we told each NavLink to route to
.
Creating a Home Route + Component
Next we need to define a Home
route for when users first arrive to the app (or when the path='/'
). For now we’ll just do a basic welcome message:
//Home.js
import React from 'react';
const Home = () => {
return (
<section>
<h1>Welcome!</h1>
<h4>Click on the links above to see a variety of creatures</h4>
</section>
)
}
export default Home;
Now let’s define the route:
//App.js
<Route path='/' component={Home} />
You Do
Next we need to define those routes and tell it which components to render. Take 10 minutes and see if you can get the /unicorns
Route working by displaying <h1> Unicorns! </h1>
.
hint: You’ll probably need to create a new component to render when on the /unicorns
route
Using the component render method
Let’s start out with using the component
render method on a Route
:
//App.js
<Route path='/unicorns' component={Unicorns} />
At this point clicking a header link (NavLink) should change the URL and render the component associated with that Route…however, we’re still seeing the Home
component above, what’s with that?
This is where we need to use the exact
attribute on a Route
//App.js
<Route exact path='/' component={Home} />
What we’re saying by adding this attribute is that only when the path is EXACT-ly '/'
do we want to render the Home
component.
Now that we have routes defined, and a template of a component, let’s get something more fun displaying. Go to your Unicorns
component and replace what you have with this code:
Unicorns.js Boilerplate
//Unicorns.js
import React from 'react';
import unicornData from './data/unicorn-data'
import './image-display.css';
const Unicorns = () => {
const displayUnicorns = unicornData.map(unicorn => {
const { id, image } = unicorn;
return <img src={image} className='app-img' key={id} />
});
return (
<>
<h1>Unicorns!</h1>
{displayUnicorns}
</>
);
}
export default Unicorns;
We should now be able to see a bunch of unicorns displaying on the page!
Your Turn!
Take some time to mimic these steps for Puppies
and Sharks
so that each respective route shows images of their respective creatures.
A few things to keep in mind:
- Each component will share classNames and need the
image-display.css
file - Each component will need their respective data:
unicorn-data.js
puppy-data.js
shark-data.js
Let’s recap where we’re at:
- We have defined four Routes:
/
/unicorns
/puppies
/sharks
- Each route renders a different component
- The
Home
route requires anexact
attribute because all of the routes contain/
Refactor using the render method
Taking a look at this, you might notice that we have three separate components for unicorns, puppies, and sharks. This kind of defeats the purpose of React though since we are essentially displaying the same information with different data for each route. Let’s try reusing the same component to display that data!
- Step 1 - Create a new component called
Creatures
that takes one prop ofdata
- feel free to use the boilerplate below!
Creatures.js Boilerplate
// Creatures.js
import React from 'react';
import './image-display.css';
const Creatures = ({data}) => {
const displayCreatures = data.map(creature => {
const { id, image } = creature;
return <img src={image} className='app-img' key={id} />
});
return (
<>
<h1>Creatures!</h1>
{displayCreatures}
</>
);
}
export default Creatures;
- Step 2 - Pull the data that is imported inside the App and pass the data down to a
<Creatures />
component using the render attribute on the<Route />
component.
App Boilerplate
// App.js
<Route path='/unicorns' render={() => <Creatures data={unicornData} />} />
<Route path='/puppies' render={() => <Creatures data={puppyData} />} />
<Route path='/sharks' render={() => <Creatures data={sharkData} />} />
Hopefully this seems pretty straight forward so far, but what if we want to go a level deeper? When a user clicks on an image, we want to send them to a new view where they can see information specific to that creature only. This is where we get into dynamic routing.
render Solution full
//App.js
import React, { Component } from 'react';
import { Route, NavLink } from 'react-router-dom'
import Home from './Home.js';
import Unicorns from './Unicorns.js';
import Puppies from './Puppies.js';
import Creatures from './Creatures.js';
import unicornData from './data/unicorn-data'
import puppyData from './data/puppy-data'
import sharkData from './data/shark-data'
import './App.css';
export default class App extends Component {
render() {
return (
<main className="App">
<header>
<NavLink to='/unicorns' className='nav'> Unicorns </NavLink>
<NavLink to='/puppies' className='nav'> Puppies </NavLink>
<NavLink to='/sharks' className='nav'> Sharks </NavLink>
</header>
<Route path='/unicorns' render={() => <Creatures data={unicornData} />} />
<Route path='/puppies' render={() => <Creatures data={puppyData} />} />
<Route path='/sharks' render={() => <Creatures data={sharkData} />} />
</main>
);
}
}
Dynamic Routing
Currently we have 9 creatures per component and we want to be able to link to a specific view for each creature. One way we could do this is to create a route for each creature…something like:
<Route path='/unicorns/1' />
<Route path='/unicorns/2' />
<Route path='/unicorns/3' />
<Route path='/unicorns/4' />
...
This would be incredibly inefficient. Instead, we can use the render
attribute within our Route
to dynamically match the id of the URL with the matching ID within our data.
To signify a dynamic route, you simply add a colon in front of the parameter you’re dynamically changing.
path='/unicorns/:id'
Let’s focus just on unicorns for now. Here are the steps we’re working through:
- A user clicks a specific image
- We
Link
to a dynamic route where the end of the URL matches the ID of that specific unicorn - Based on the ID in the URL, we pass through data specific to that matching unicorn
So, if our first unicorn’s data looks like this:
{
id: 1,
name: 'Chuck',
image: img1,
type: 'unicorns',
bio: bio1
}
We want to redirect to /unicorns/1
Then, we want to define a Route
that looks at the parameter in the URL and passes the specific matching data into something we can render.
Let’s poke the bear a little bit. Paste this route into your App.js
file:
//App.js
<Route path='/unicorns/:id' render={({ match }) => {
console.log(match)
return (
<div>New Unicorn Route!</div>
)
}} />
Now visit this URL and open up your console: http://localhost:3000/unicorns/1
First thing we should see is that all of our unicorns are still showing, why do you think this is?
It’s because we didn’t specify the exact
attribute in our /unicorns
route, so that route sees the URL is /unicorns/1
, considers it a match and renders any components that match. Let’s fix this for all three components:
//App.js
<Route exact path='/unicorns' render={() => <Creatures data={unicornData} />} />
<Route exact path='/puppies' render={() => <Creatures data={puppyData} />} />
<Route exact path='/sharks' render={() => <Creatures data={sharkData} />} />
Ok back to business!
We passed through the match
prop and console logged it, let’s take a look. Notice that the params
property contains an object with our defined parameter id
, and it is equal to the ID we linked to in the URL!
Now all we have to do is modify our route to render the correct data.
Your Turn!
See if you can write some code to render the correct data based on the ID in the URL. Focus just on unicorns for now.
Hints:
- You will need to bring in the data from the
unicorn-data.js
file - Use the
CreatureDetails
component already set up for you! - Focus first on seeing the data when you type in the URL manually, we’ll set up the click next
Dynamic Route Solution
<Route path='/unicorns/:id' render={({ match }) => {
const { id } = match.params
const creature = unicornData.find(uni => uni.id === parseInt(id))
return creature && <CreatureDetails {...creature} />
}} />
Now if we visit http://localhost:3000/unicorns/1
we should see a view specifically for Unicorn 1!
Your Turn!
See if you can modify your Creatures
component so that each image can be clicked!
- Utilize a
<Link>
to wrap around the JSX we already have - How can you hook into the data passed down to creature to build the correct path for each
<Link>
?
Clickable Link Solution
import { Link } from 'react-router-dom'
const displayCreatures = data.map(creature => {
const { id, image, type } = creature;
return (
<Link to={`/${type}/${id}`} key={id}>
<img src={image} className='app-img' />
</Link>
)
});
And that’s it! Go ahead and work on setting up dynamic routes for the other two components!