- Describe the difference between server-side and client-side routing
- Add client-side routing to our React apps using React Router
- Set up React Router to enable common browser behavior
- Understand built in React Router components (Route, Router, Switch, Link, Redirect)
Client-side routing
is a bit of a misnomer.
On the server, routing
generally refers to the way we
define the URLs and RESTful resources that make up our application. Whether
we are asking for data from the database or persisting data, our server
needs to know where the data lives. Server routes
help us keep track of this information.
In the browser, things are a little different. When we build Single-Page Applications, we render
our data inside of the browser. The data lives on a server, so our data's "addresses" have
been defined elsewhere. We only need to know what these pre-defined addresses are to consume them.
We'll still have a lot of different views
for our data, and we won't want to
show all of them on the page at once. Client-side routing
is how we'll describe
which views we are showing on the page at any given time.
There is no way to handle client-side routing in the React library. Instead, there are multiple libraries available to handle this specific task. The most popular library is called React Router
. This library is hands down the most popular solution for client-side routing. Recently, React Router
made a major overhaul to their library with the release of v4
. In the newest version, the team behind React Router
refactored the library to make everything just a regular React components. Some online tutorials have not yet updated their libraries, so we recommend using the official docs
React Router is going to allow us to swap out sets of components using familiar
"routing" patterns, rather than writing lots of complicated if-statements
in our JavaScript.
To demonstrate the power of this tool, we're going to build a personal banking application, where we can independently display the Debits and Credits made to each account.
Before we jump into React Router, let's create a fresh React application in our class exercises folder:
$ create-react-app bank-of-react
Let's start up our application and make sure everything looks good before we move on.
React Router is packaged up nicely in npm
. We can use npm
to quickly pull the Router
code into our application. The package we'll need is react-router-dom
:
$ npm i react-router-dom
-
Now that we've pulled in the React Router code, we need to create a new Router within our application. Because React is not a browser-only framework, the React Router team has provided for us a few different routers, specific to each environment. For our browser-rendered React applications, we'll want to use the
BrowserRouter
.Let's go into our
App.js
and pull in theBrowserRouter
as a component calledRouter
. Let's also take the opportunity to clear out our React boilerplate and replace it with "Hello, World!":
import React, {Component} from 'react';
import {BrowserRouter as Router} from 'react-router-dom';
class App extends Component {
render() {
return (
<div className="App">
Hello, World!
</div>
);
}
}
export default App;
-
Once we've imported our Router, the only set-up left is to wrap our top-level component inside of a
<Router />
component, like so:import React, {Component} from 'react'; import {BrowserRouter as Router} from 'react-router-dom'; class App extends Component { render() { return ( <Router> <div className="App"> Hello, World! </div> </Router> ); } } export default App;
-
With a few very simple lines of code, all of the goodness of React Router becomes available to us. Let's explore!
- Let's add a Home component to our application. The first thing we'll need to do is create our component:
// src/components/Home.js
import React, { Component } from 'react';
class Home extends Component {
render() {
return (
<div>
<img src="https://letstalkpayments.com/wp-content/uploads/2016/04/Bank.png" alt="bank"/>
<h1>Bank of React</h1>
</div>
);
}
}
export default Home;
-
Now that we have a nicely packaged
Home
component, we'll want to display this component on the page instead of the obligatory "Hello, World!". We could simply mount this component on theApp.js
component, like we've done in the past. This would work absolutely fine, but what will happen once we've navigated away from this page?How would we get back to the very simple, static
Home
page that we've set up for ourselves? -
We could create a "flag" on the
state
of the main component calledshowHomePage
and whenevershowHomePage === true
we'd replace everything else with theHome
component. This would work, but in the long run we'd have to passshowHomePage
as aprop
to any component that needed to link back to theHome
component.This may seem doable for a small application. However, if we were to "scale" this to any reasonably-sized app, we'd quickly see that every component would need
if-statements
for each and every page it could possibly link to. Our component structure would quickly become un-manageable. We haven't even displayed ourHome
page yet, and the need for React Router has already set in... -
Instead of simply mounting our
Home
component, let's instead assign aRoute
to it. We can do this very simply by importing theRoute
component fromreact-router-dom
and mounting it within ourApp.js
. Let's try it out:import React, {Component} from 'react'; import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'; import Home from './components/Home'; class App extends Component { render() { return ( <Router> <Switch> <Route exact path="/" component={Home}/> </Switch> </Router> ); } } export default App;
-
If we check the browser, we should now see our Home page rendered instead of the previous "Hello, World!". Let's break down the new syntax:
... <Route exact path="/" component={Home}/> ...
-
The first thing we should notice is that adding
Routes
to our application is as simple as mounting any other component. All we have to do is import it and mount it with some props passed in. The props that we are using tell theRoute
to mount aHome
component any time the browser sees a request to exactly the '/' route. Theexact
prop is very important, so the browser doesn't show ourHome
page when we add any other paths after the initial/
.
By Default, more than one route can be rendered at a time. We don't have a use case to show multiple routes, so we will wrap our code in a <Switch>
component to make sure we only show the results of one route at a time. For more info about switch, check out this link
-
Now let's add some
props
to ourHome
component! On our banking home page, we'll want to view our account balance. Let's create anAccountBalance
component and pass the balance as aprop
. We'll want to use this information elsewhere, so we'll keep the value on thestate
of ourApp.js
.// src/components/AccountBalance.js import React, {Component} from 'react'; class AccountBalance extends Component { render() { return ( <div> Balance: {this.props.accountBalance} </div> ); } } export default AccountBalance;
// src/components/Home.js import React, {Component} from 'react'; import AccountBalance from './AccountBalance'; class Home extends Component { render() { return ( <div> <img src="https://letstalkpayments.com/wp-content/uploads/2016/04/Bank.png" alt="bank"/> <h1>Bank of React</h1> <AccountBalance accountBalance={this.props.accountBalance}/> </div> ); } } export default Home;
// src/components/App.js ... constructor() { super(); this.state = { accountBalance: 14568.27 } } ...
-
Now we have all of the pieces set up. We just need to tell our
"/" Route
to pass theaccountBalance
as a prop each time it loads theHome
component. We might try something like this:<Route exact path="/" component={<Home accountBalance={this.state.accountBalance}/>}/>
-
...but if we check the browser we'll see that this throws an error.
Route
components expect us to pass a "reference" to a component as ourcomponent=
prop. This means we can't pass an already-built component, like the one above. Fortunately, there is a very simple alternative:
...
render() {
const HomeComponent = () => (<Home accountBalance={this.state.accountBalance}/>);
return (
<Router>
<div>
<Route exact path="/" render={HomeComponent}/>
</div>
</Router>
);
}
...
-
The
component
prop won't accept a pre-built component, but React Router has provided an alternativerender
method that will. All we have to do is save our component as a variable and pass it straight in as aprop
.Now we can refresh the page and see our
Home
page with a properAccountBalance
.- Notice the difference between the
render
andcomponent
prop. If you need to pass props at theRouter
level, you must use render... otherwise use thecomponent
prop.
- Notice the difference between the
-
Now that we have set up a Route for our
Home
component, we can navigate back to the component at any time. Let's build out aUserProfile
page and create aRoute
for it in ourApp.js
:// src/components/UserProfile.js import React, {Component} from 'react'; class UserProfile extends Component { render() { return ( <div> <h1>User Profile</h1> <div>Username: {this.props.userName}</div> <div>Member Since: {this.props.memberSince}</div> </div> ); } } export default UserProfile;
// src/App.js import React, {Component} from 'react'; import {BrowserRouter as Router, Route} from 'react-router-dom'; import Home from './components/Home'; import UserProfile from './components/UserProfile'; class App extends Component { constructor() { super(); this.state = { accountBalance: 14568.27, currentUser: { userName: 'bob_loblaw', memberSince: '08/23/99', } } } render() { const HomeComponent = () => (<Home accountBalance={this.state.accountBalance}/>); const UserProfileComponent = () => ( <UserProfile userName={this.state.currentUser.userName} memberSince={this.state.currentUser.memberSince} /> ); return ( <Router> <div> <Route exact path="/" render={HomeComponent}/> <Route exact path="/userProfile" render={UserProfileComponent}/> </div> </Router> ); } } export default App;
-
If we type
http://localhost:3000/userProfile
into our browser, we see our newUserProfile
on the screen. This isn't a great user experience, obviously. Let's instead add a link to ourUserProfile
from theHome
component. React Router provides another very simple solution for this: theLink
component.import React, {Component} from 'react'; import AccountBalance from './AccountBalance'; import {Link} from 'react-router-dom'; class Home extends Component { render() { return ( <div> <img src="https://letstalkpayments.com/wp-content/uploads/2016/04/Bank.png" alt="bank"/> <h1>Bank of React</h1> <Link to="/userProfile">User Profile</Link> <AccountBalance accountBalance={this.props.accountBalance}/> </div> ); } } export default Home;
-
React Router's
Link
component takes a very simple propto=
that tells us where we want to redirect. We can link to any of our components, even within child components, as long as aRoute
has been initialized next to or above theLink
in our tree of components.
- We are now able to view our
UserProfile
component through theHome
page link, but we are left stranded once we get there. Let's improve our user experience by adding a link back to theHome
component inside of theUserProfile
component.
While the <Link>
component is really powerful, it requires a user interaction to navigate to another route. There are plenty of use cases that require a redirect outside of user clicks. One common example is wanting to redirect after a form is submitted. When this happens, we need to use the <Redirect>
component that is build into React Router. Let's take a look at this mock Sign In
component that we can add to our bank.
// App.js
...
mockLogIn = (logInInfo) => {
const newUser = {...this.state.currentUser}
newUser.userName = logInInfo.userName
this.setState({currentUser: newUser})
}
...
const LogInComponent = () => (<LogIn user={this.state.currentUser} mockLogIn={this.mockLogIn} {...this.props}/>)
...
<Route exact path="/login" render={LogInComponent}/>
...
// Login.js
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
class LogIn extends Component {
constructor () {
super()
this.state = {
user: {
userName: '',
password: ''
},
redirect: false
}
}
handleChange = (e) => {
const updatedUser = {...this.state.user}
const inputField = e.target.name
const inputValue = e.target.value
updatedUser[inputField] = inputValue
this.setState({user: updatedUser})
}
handleSubmit = (e) => {
e.preventDefault()
this.props.mockLogIn(this.state.user)
this.setState({redirect: true})
}
render () {
if (this.state.redirect) {
return (<Redirect to="/userProfile"/>)
}
return (
<div>
<form onSubmit={this.handleSubmit}>
<div>
<label htmlFor="userName">User Name</label>
<input type="text" name="userName" onChange={this.handleChange} value={this.state.user.userName} />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" name="password" />
</div>
<button>Log In</button>
</form>
</div>
)
}
}
export default LogIn
Let's add some more features to our banking app, using the following User Stories
!
- The Debits index endpoint is located at
https://bank-of-react-bxbys1cq8-ajlapid718.vercel.app/debits
- The Credits index endpoint is located at
https://bank-of-react-bxbys1cq8-ajlapid718.vercel.app/credits
Making the Account Balance dynamic:
GIVEN I am on any page displaying the Account Balance
WHEN I view the Account Balance display area
THEN I should see an Account Balance that accurately represents my Debits subtracted from my Credits
AND I should be able to see a negative account balance if I have more Debits than Credits
Viewing the Debits page:
GIVEN I am on the Home Page
WHEN I click on 'Debits'
THEN I should be redirected to the Debits page
AND I should see a title of 'Debits' on the page
Displaying debits:
GIVEN I am on the Debits page
WHEN I view the Debits display area
THEN I should see all of my debits displayed
AND each Debit should display a Debit description
AND each Debit should display a Debit amount
AND each Debit should display a Debit date
Viewing the Account Balance on the Debits page:
GIVEN I am on the Debits page
WHEN I view the Account Balance display area
THEN I should see my Account Balance displayed
Adding debits:
GIVEN I am on the Debits page
WHEN I enter a new Debit description
AND WHEN I enter a new Debit amount
AND WHEN I click 'Add Debit'
THEN I should see my new debit added to the Debits display area with the current date
AND I should see my Account Balance updated to reflect the new Debit
Viewing the Account Balance on the Debits page:
GIVEN I am on the Debits page
WHEN I view the Account Balance display area
THEN I should see my Account Balance displayed
Viewing the Credits page:
GIVEN I am on the Home Page
WHEN I click on 'Credits'
THEN I should be redirected to the Credits page
AND I should see a title of 'Credits' on the page
Displaying debits:
GIVEN I am on the Credits page
WHEN I view the Credits display area
THEN I should see all of my Credits displayed
AND each Debit should display a Debit description
AND each Debit should display a Debit amount
AND each Debit should display a Debit date
Viewing the Account Balance on the Credits page:
GIVEN I am on the Credits page
WHEN I view the Account Balance display area
THEN I should see my Account Balance displayed
Adding Credits:
GIVEN I am on the Credits page
WHEN I enter a new Debit description
AND WHEN I enter a new Debit amount
AND WHEN I click 'Add Debit'
THEN I should see my new debit added to the Credits display area with the current date
AND I should see my Account Balance updated to reflect the new Debit
Viewing the Account Balance on the Credits page:
GIVEN I am on the Credits page
WHEN I view the Account Balance display area
THEN I should see my Account Balance displayed