React Routing 2 - my version of the React Router Tutorial
This is based on the React Router Tutorial but built with the app-app
boilerplate by @ambethia . I used the "delta" stack for this project.
Below are all the notes from the original tutorial that I have recompiled into this one file. See below...
First you'll need Node.js and the package manager that comes with it: npm.
Once you've got that working, head to the command line where we'll set up our project.
Clone the Tutorial
git clone https://github.com/reactjs/react-router-tutorial cd react-router-tutorial cd lessons/01-setting-up npm install npm start Now open up http://localhost:8080
Feel free to poke around the code to see how we're using webpack and npm scripts to run the app.
You should see a "Hello React Router" message in the browser.
Make Some Changes
Open up modules/App.js and change the text to something like "Hello ". The browser automatically reloads with your new code.
At its heart, React Router is a component.
render(<Router/>, document.getElementById('app'))
That's not going to display anything until we configure a route.
Open up index.js
and
- import
Router
,Route
, andhashHistory
- render a
Router
instead ofApp
// ...
import { Router, Route, hashHistory } from 'react-router'
render((
<Router history={hashHistory}>
<Route path="/" component={App}/>
</Router>
), document.getElementById('app'))
Make sure your server is running with npm start
and then visit
http://localhost:8080
You should get the same screen as before, but this time with some junk
in the URL. We're using hashHistory
--it manages the routing history
with the hash portion of the url. It's got that extra junk to shim some
behavior the browser has natively when using real urls. We'll change
this to use real urls later and lose the junk, but for now, this works
great because it doesn't require any server-side configuration.
Create two new components at:
modules/About.js
modules/Repos.js
// modules/About.js
import React from 'react'
export default React.createClass({
render() {
return <div>About</div>
}
})
// modules/Repos.js
import React from 'react'
export default React.createClass({
render() {
return <div>Repos</div>
}
})
Now we can couple them to the app at their respective paths.
// insert into index.js
import About from './modules/About'
import Repos from './modules/Repos'
render((
<Router history={hashHistory}>
<Route path="/" component={App}/>
{/* add the routes here */}
<Route path="/repos" component={Repos}/>
<Route path="/about" component={About}/>
</Router>
), document.getElementById('app'))
Now visit http://localhost:8080/#/about and http://localhost:8080/#/repos
Perhaps the most used component in your app is Link
. It's almost
identical to the <a/>
tag you're used to except that it's aware of
the Router
it was rendered in.
Let's create some navigation in our App
component.
// modules/App.js
import React from 'react'
import { Link } from 'react-router'
export default React.createClass({
render() {
return (
<div>
<h1>React Router Tutorial</h1>
<ul role="nav">
<li><Link to="/about">About</Link></li>
<li><Link to="/repos">Repos</Link></li>
</ul>
</div>
)
}
})
Now visit http://localhost:8080 and click the links, click back, click forward. It works!
The navigation we added to App
should probably be present on every
screen. Without React Router, we could wrap that ul
into a
component, say Nav
, and render a Nav
on every one of our screens.
This approach isn't as clean as the application grows. React Router provides another way to share UI like this with nested routes, a trick it learned from Ember (/me tips hat).
Have you ever noticed your app is just a series of boxes inside boxes
inside boxes? Have you also noticed your URLs tend to be coupled to that
nesting? For example given this url, /repos/123
, our
components would probably look like this:
<App> {/* / */}
<Repos> {/* /repos */}
<Repo/> {/* /repos/123 */}
</Repos>
</App>
And our UI something like:
+-------------------------------------+
| Home Repos About | <- App
+------+------------------------------+
| | |
Repos -> | repo | Repo 1 |
| | |
| repo | Boxes inside boxes |
| | inside boxes ... | <- Repo
| repo | |
| | |
| repo | |
| | |
+------+------------------------------+
React Router embraces this by letting you nest your routes, which automatically becomes nested UI.
Let's nest our About
and Repos
components inside of App
so that we
can share the navigation with all screens in the app. We do it in two
steps:
First, let the App
Route
have children, and move the other routes
underneath it.
// index.js
// ...
render((
<Router history={hashHistory}>
<Route path="/" component={App}>
{/* make them children of `App` */}
<Route path="/repos" component={Repos}/>
<Route path="/about" component={About}/>
</Route>
</Router>
), document.getElementById('app'))
Next, render children inside of App
.
// modules/App.js
// ...
render() {
return (
<div>
<h1>React Router Tutorial</h1>
<ul role="nav">
<li><Link to="/about">About</Link></li>
<li><Link to="/repos">Repos</Link></li>
</ul>
{/* add this */}
{this.props.children}
</div>
)
}
// ...
Alright, now go click the links and notice that the App
component
continues to render while the child route's component gets swapped
around as this.props.children
:)
React Router is constructing your UI like this:
// at /about
<App>
<About/>
</App>
// at /repos
<App>
<Repos/>
</App>
The best way to build large things is to stitch small things together.
This is the real power of React Router, every route can be developed (even rendered!) as an independent application. Your route configuration stitches all these apps together however you'd like. Applications inside of Applications, boxes inside of boxes.
What happens if you move the About
route outside of App
?
Okay, now put it back.
One way that Link
is different from a
is that it knows if the path
it links to is active so you can style it differently.
Let's see how it looks with inline styles, add activeStyle
to your
Link
s.
// modules/App.js
<li><Link to="/about" activeStyle={{ color: 'red' }}>About</Link></li>
<li><Link to="/repos" activeStyle={{ color: 'red' }}>Repos</Link></li>
Now as you navigate, the active link is red.
You can also use an active class name instead of inline-styles.
// modules/App.js
<li><Link to="/about" activeClassName="active">About</Link></li>
<li><Link to="/repos" activeClassName="active">Repos</Link></li>
We don't have a stylesheet on the page yet though. Lets add one-extra
point if you can add a link
tag from memory.
// index.html
<link rel="stylesheet" href="index.css" />
And the CSS file:
.active {
color: green;
}
You'll need to manually refresh the browser since Webpack isn't building
our index.html
.
Most links in your site don't need to know they are active, usually just
primary navigation links need to know. It's useful to wrap those so you
don't have to remember what your activeClassName
or activeStyle
is
everywhere.
We will use a spread operator here, the three dots. It clones our props
and in this use case it clones activeClassName
to our desired component for
us to benefit from.
Create a new file at modules/NavLink.js
that looks like this:
// modules/NavLink.js
import React from 'react'
import { Link } from 'react-router'
export default React.createClass({
render() {
return <Link {...this.props} activeClassName="active"/>
}
})
Now you can go change your links to NavLink
s.
// modules/App.js
import NavLink from './NavLink'
// ...
<li><NavLink to="/about">About</NavLink></li>
<li><NavLink to="/repos">Repos</NavLink></li>
Oh, how beautiful upon the renders is the composability of components.
Consider the following URLs:
/repos/reactjs/react-router
/repos/facebook/react
These URLs would match a route path like this:
/repos/:userName/:repoName
The parts that start with :
are URL parameters whose values will be
parsed out and made available to route components on
this.props.params[name]
.
Let's teach our app how to render screens at /repos/:userName/:repoName
.
First we need a component to render at the route, make a new file at
modules/Repo.js
that looks something like this:
// modules/Repo.js
import React from 'react'
export default React.createClass({
render() {
return (
<div>
<h2>{this.props.params.repoName}</h2>
</div>
)
}
})
Now open up index.js
and add the new route.
// ...
// import Repo
import Repo from './modules/Repo'
render((
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="/repos" component={Repos}/>
{/* add the new route */}
<Route path="/repos/:userName/:repoName" component={Repo}/>
<Route path="/about" component={About}/>
</Route>
</Router>
), document.getElementById('app'))
Now we can add some links to this new route in Repos.js
.
// Repos.js
import { Link } from 'react-router'
// ...
export default React.createClass({
render() {
return (
<div>
<h2>Repos</h2>
{/* add some links */}
<ul>
<li><Link to="/repos/reactjs/react-router">React Router</Link></li>
<li><Link to="/repos/facebook/react">React</Link></li>
</ul>
</div>
)
}
})
Now go test your links out. Note that the parameter name in the route
path
becomes the property name in the component. Both repoName
and
userName
are available on this.props.params
of your component. You
should probably add some prop types to help others and yourself out
later.
Notice how the list of links to different repositories goes away when we navigate to a repository? What if we want the list to persist, just like the global navigation persists?
Try to figure that out before reading on.
...
First, nest the Repo
route under the Repos
route. Then go render
this.props.children
in Repos
.
// index.js
// ...
<Route path="/repos" component={Repos}>
<Route path="/repos/:userName/:repoName" component={Repo}/>
</Route>
// Repos.js
// ...
<div>
<h2>Repos</h2>
<ul>
<li><Link to="/repos/reactjs/react-router">React Router</Link></li>
<li><Link to="/repos/facebook/react">React</Link></li>
</ul>
{/* will render `Repo.js` when at /repos/:userName/:repoName */}
{this.props.children}
</div>
Let's bring in our NavLink
from before so we can add the active
class name to these links:
// modules/Repos.js
// import it
import NavLink from './NavLink'
// ...
<li><NavLink to="/repos/reactjs/react-router">React Router</NavLink></li>
<li><NavLink to="/repos/facebook/react">React</NavLink></li>
// ...
Notice how both the /repos
link up top and the individual repo links are
both active? When child routes are active, so are the parents.
When we visit /
in this app it's just our navigation and a blank page.
We'd like to render a Home
component there. Lets create a Home
component and then talk about how to render it at /
.
// modules/Home.js
import React from 'react'
export default React.createClass({
render() {
return <div>Home</div>
}
})
One option is to see if we have any children in App
, and if not,
render Home
:
// modules/App.js
import Home from './Home'
// ...
<div>
{/* ... */}
{this.props.children || <Home/>}
</div>
//...
This would work fine, but its likely we'll want Home
to be attached to
a route like About
and Repos
in the future. A few reasons include:
- Participating in a data fetching abstraction that relies on matched routes and their components.
- Participating in
onEnter
hooks - Participating in code-splitting
Also, it just feels good to keep App
decoupled from Home
and let the
route config decide what to render as the children. Remember, we want to
build small apps inside small apps, not big ones!
Let's add a new route to index.js
.
// index.js
// new imports:
// add `IndexRoute` to 'react-router' imports
import { Router, Route, hashHistory, IndexRoute } from 'react-router'
// and the Home component
import Home from './modules/Home'
// ...
render((
<Router history={hashHistory}>
<Route path="/" component={App}>
{/* add it here, as a child of `/` */}
<IndexRoute component={Home}/>
<Route path="/repos" component={Repos}>
<Route path="/repos/:userName/:repoName" component={Repo}/>
</Route>
<Route path="/about" component={About}/>
</Route>
</Router>
), document.getElementById('app'))
Now open http://localhost:8080 and you'll see the new component is rendered.
Notice how the IndexRoute
has no path. It becomes
this.props.children
of the parent when no other child of the parent
matches, or in other words, when the parent's route matches exactly.
Index routes can twist people's brains up sometimes. Hopefully it will
sink in with a bit more time. Just think about a web server that looks
for index.html
when you're at /
. Same idea, React Router looks for
an index route if a route's path matches exactly.
Have you noticed in our app that we don't have any navigation to get
back to rendering the Home
component?
Lets add a link to /
and see what happens:
// in App.js
// ...
<li><NavLink to="/">Home</NavLink></li>
// ...
Now navigate around. Notice anything weird? The link to Home
is always
active! As we learned earlier, parent routes are active when child routes
are active. Unfortunately, /
is the parent of everything.
For this link, we want it to only be active when the index route is active. There are two ways to let the router know you're linking to the "index route" so it only adds the active class (or styles) when the index route is rendered.
First let's use the IndexLink
instead of NavLink
// App.js
import { IndexLink } from 'react-router'
// ...
<li><IndexLink to="/" activeClassName="active">Home</IndexLink></li>
Fixed! Now this link is only "active" when we're at the index route. Go ahead and click around to see.
We can use Link
as well by passing it the onlyActiveOnIndex
prop
(IndexLink
just wraps Link
with this property for convenience).
<li><Link to="/" activeClassName="active" onlyActiveOnIndex={true}>Home</Link></li>
That's fine, but we already abstracted away having to know what the
activeClassName
is with Nav
.
Remember, in NavLink
we're passing along all of our props to Link
with
the {...spread}
syntax, so we can actually add the prop when we render
a NavLink
and it will make its way down to the Link
:
<li><NavLink to="/" onlyActiveOnIndex={true}>Home</NavLink></li>
The URLs in our app right now are built on a hack: the hash. It's the default because it will always work, but there's a better way.
Modern browsers let JavaScript manipulate the URL without making an http
request, so we don't need to rely on the hash (#
) portion of the url
to do routing, but there's a catch (we'll get to it later).
Open up index.js
and import browserHistory
instead of hashHistory
.
// index.js
// ...
// bring in `browserHistory` instead of `hashHistory`
import { Router, Route, browserHistory, IndexRoute } from 'react-router'
render((
<Router history={browserHistory}>
{/* ... */}
</Router>
), document.getElementById('app'))
Now go click around and admire your clean URLs.
Oh yeah, the catch. Click on a link and then refresh your browser. What happens?
Cannot GET /repos
Your server needs to deliver your app no matter what URL comes in, because your app, in the browser, is manipulating the URL. Our current server doesn't know how to handle the URL.
The Webpack Dev Server has an option to enable this. Open up
package.json
and add --history-api-fallback
.
"start": "webpack-dev-server --inline --content-base . --history-api-fallback"
We also need to change our relative paths to absolute paths in
index.html
since the URLs will be at deep paths and the app, if it
starts at a deep path, won't be able to find the files.
<!-- index.html -->
<!-- index.css -> /index.css -->
<link rel="stylesheet" href="/index.css">
<!-- bundle.js -> /bundle.js -->
<script src="/bundle.js"></script>
Stop your server if it's running, then npm start
again. Look at those
clean URLs :)
None of this has anything to do with React Router, but since we're talking about web servers, we might as well take it one step closer to the real-world. We'll also need it for server rendering in the next section.
Webpack dev server is not a production server. Let's make a production server and a little environment-aware script to boot up the right server depending on the environment.
Let's install a couple modules:
npm install express if-env compression --save
First, we'll use the handy if-env
in package.json
. Update your
scripts entry in package.json to look like this:
// package.json
"scripts": {
"start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
"start:dev": "webpack-dev-server --inline --content-base . --history-api-fallback",
"start:prod": "webpack && node server.js"
},
In the root directly, go open up webpack.config.js
and add the publicPath '/' as per below:
// webpack.config.js
output: {
path: 'public',
filename: 'bundle.js',
publicPath: '/'
},
When you run npm start
it checks if the value of our NODE_ENV
environment variable is
production
. If yes, it runs npm run start:prod
, if not, it runs
npm run start:dev
.
Now we're ready to create a production server with Express and add a new file at root dir. Here's a first attempt:
// server.js
var express = require('express')
var path = require('path')
var app = express()
// serve our static stuff like index.css
app.use(express.static(__dirname))
// send all requests to index.html so browserHistory in React Router works
app.get('*', function (req, res) {
res.sendFile(path.join(__dirname, 'index.html'))
})
var PORT = process.env.PORT || 8080
app.listen(PORT, function() {
console.log('Production Express server running at localhost:' + PORT)
})
Now run:
NODE_ENV=production npm start
# For Windows users:
# SET "NODE_ENV=production" && npm start
Congratulations! You now have a production server for this app. After clicking around, try navigating to http://localhost:8080/package.json. Whoops. Let's fix that. We're going to shuffle around a couple files and update some paths scattered across the app.
- make a
public
directory. - Move
index.html
andindex.css
into it.
Now let's update server.js
to point to the right directory for static
assets:
// server.js
// ...
// add path.join here
app.use(express.static(path.join(__dirname, 'public')))
// ...
app.get('*', function (req, res) {
// and drop 'public' in the middle of here
res.sendFile(path.join(__dirname, 'public', 'index.html'))
})
We also need to tell webpack to build to this new directory:
// webpack.config.js
// ...
output: {
path: 'public',
// ...
}
And finally (!) add it to the --content-base
argument to npm run start:dev
script:
"start:dev": "webpack-dev-server --inline --content-base public --history-api-fallback",
If we had the time in this tutorial, we could use the WebpackDevServer
API in a JavaScript file instead of the CLI in an npm script and then
turn this path into config shared across all of these files. But, we're
already on a tangent, so that will have to wait for another time.
Okay, now that we aren't serving up the root of our project as public files, let's add some code minification to Webpack and gzipping to express.
// webpack.config.js
// make sure to import this
var webpack = require('webpack')
module.exports = {
// ...
// add this handful of plugins that optimize the build
// when we're in production
plugins: process.env.NODE_ENV === 'production' ? [
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin()
] : [],
// ...
}
And compression in express:
// server.js
// ...
var compression = require('compression')
var app = express()
// must be first!
app.use(compression())
Now go start your server in production mode:
NODE_ENV=production npm start
You'll see some UglifyJS logging and then in the browser, you can see the assets are being served with gzip compression.
While most navigation happens with Link
, you can programmatically
navigate around an application in response to form submissions, button
clicks, etc.
Let's make a little form in Repos
that programmatically navigates.
// modules/Repos.js
import React from 'react'
import NavLink from './NavLink'
export default React.createClass({
// add this method
handleSubmit(event) {
event.preventDefault()
const userName = event.target.elements[0].value
const repo = event.target.elements[1].value
const path = `/repos/${userName}/${repo}`
console.log(path)
},
render() {
return (
<div>
<h2>Repos</h2>
<ul>
<li><NavLink to="/repos/reactjs/react-router">React Router</NavLink></li>
<li><NavLink to="/repos/facebook/react">React</NavLink></li>
{/* add this form */}
<li>
<form onSubmit={this.handleSubmit}>
<input type="text" placeholder="userName"/> / {' '}
<input type="text" placeholder="repo"/>{' '}
<button type="submit">Go</button>
</form>
</li>
</ul>
{this.props.children}
</div>
)
}
})
There are two ways you can do this, the first is simpler than the second.
First we can use the browserHistory
singleton that we passed into
Router
in index.js
and push a new URL into the history.
// modules/Repos.js
import { browserHistory } from 'react-router'
// ...
handleSubmit(event) {
// ...
const path = `/repos/${userName}/${repo}`
browserHistory.push(path)
},
// ...
There's a potential problem with this though. If you pass a different
history to Router
than you use here, it won't work. It's not very
common to use anything other than browserHistory
, so this is
acceptable practice. If you're concerned about it, you can make a module
that exports the history you want to use across the app, or...
You can also use the router
that Router
provides on "context".
First, you ask for context in the component, and then you can use it:
export default React.createClass({
// ask for `router` from context
contextTypes: {
router: React.PropTypes.object
},
// ...
handleSubmit(event) {
// ...
this.context.router.push(path)
},
// ..
})
This way you'll be sure to be pushing to whatever history gets passed to
Router
. It also makes testing a bit easier since you can more easily
stub context than singletons.
Alright, first things first. Server rendering, at its core is a simple concept in React.
render(<App/>, domNode)
// can be rendered on the server as
const markup = renderToString(<App/>)
It's not rocket science, but it also isn't trivial. First I'm going to just throw a bunch of webpack shenanigans at you with little explanation, then we'll talk about the Router.
Since node doesn't (and shouldn't) understand JSX, we need to compile
the code somehow. Using something like babel/register
is not fit for
production use, so we'll use webpack to build a server bundle, just like
we use it to build a client bundle.
Make a new file called webpack.server.config.js
and put this stuff in
there:
var fs = require('fs')
var path = require('path')
module.exports = {
entry: path.resolve(__dirname, 'server.js'),
output: {
filename: 'server.bundle.js'
},
target: 'node',
// keep node_module paths out of the bundle
externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([
'react-dom/server', 'react/addons',
]).reduce(function (ext, mod) {
ext[mod] = 'commonjs ' + mod
return ext
}, {}),
node: {
__filename: true,
__dirname: true
},
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' }
]
}
}
Hopefully some of that makes sense, we aren't going to cover what all of
that stuff does, it's sufficient to say that now we can run our
server.js
file through webpack and then run it.
Now we need to make some scripts to build server bundle before we try to
run our app. Update your package.json
script config to look like
this:
"scripts": {
"start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
"start:dev": "webpack-dev-server --inline --content-base public/ --history-api-fallback",
"start:prod": "npm run build && node server.bundle.js",
"build:client": "webpack",
"build:server": "webpack --config webpack.server.config.js",
"build": "npm run build:client && npm run build:server"
},
Now when we run NODE_ENV=production npm start
both the client and
server bundles get created by Webpack.
Okay, let's talk about the Router. We're going to need our routes split
out into a module so that both the client and server entries can require
it. Make a file at modules/routes
and move your routes and components
into it.
// modules/routes.js
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './App'
import About from './About'
import Repos from './Repos'
import Repo from './Repo'
import Home from './Home'
module.exports = (
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/repos" component={Repos}>
<Route path="/repos/:userName/:repoName" component={Repo}/>
</Route>
<Route path="/about" component={About}/>
</Route>
)
// index.js
import React from 'react'
import { render } from 'react-dom'
import { Router, browserHistory } from 'react-router'
// import routes and pass them into <Router/>
import routes from './modules/routes'
render(
<Router routes={routes} history={browserHistory}/>,
document.getElementById('app')
)
Now open up server.js
. We're going to bring in two modules from React
Router to help us render on the server.
If we tried to render a <Router/>
on the server like we do in the
client, we'd get an empty screen since server rendering is synchronous
and route matching is asynchronous.
Also, most apps will want to use the router to help them load data, so asynchronous routes or not, you'll want to know what screens are going to render before you actually render so you can use that information to load asynchronous data before rendering. We don't have any data loading in this app, but you'll see where it could happen.
First we import match
and RouterContext
from react router, then
we'll match the routes to the url, and finally render.
// ...
// import some new stuff
import React from 'react'
// we'll use this to render our app to an html string
import { renderToString } from 'react-dom/server'
// and these to match the url to routes and then render
import { match, RouterContext } from 'react-router'
import routes from './modules/routes'
// ...
// send all requests to index.html so browserHistory works
app.get('*', (req, res) => {
// match the routes to the url
match({ routes: routes, location: req.url }, (err, redirect, props) => {
// `RouterContext` is what the `Router` renders. `Router` keeps these
// `props` in its state as it listens to `browserHistory`. But on the
// server our app is stateless, so we need to use `match` to
// get these props before rendering.
const appHtml = renderToString(<RouterContext {...props}/>)
// dump the HTML into a template, lots of ways to do this, but none are
// really influenced by React Router, so we're just using a little
// function, `renderPage`
res.send(renderPage(appHtml))
})
})
function renderPage(appHtml) {
return `
<!doctype html public="storage">
<html>
<meta charset=utf-8/>
<title>My First React Router App</title>
<link rel=stylesheet href=/index.css>
<div id=app>${appHtml}</div>
<script src="/bundle.js"></script>
`
}
var PORT = process.env.PORT || 8080
app.listen(PORT, function() {
console.log('Production Express server running at localhost:' + PORT)
})
And that's it. Now if you run NODE_ENV=production npm start
and visit
the app, you can view source and see that the server is sending down our
app to the browser. As you click around, you'll notice the client app
has taken over and doesn't make requests to the server for UI. Pretty
cool yeah?!
Our callback to match is a little naive, here's what a production version would look like:
app.get('*', (req, res) => {
match({ routes: routes, location: req.url }, (err, redirect, props) => {
// in here we can make some decisions all at once
if (err) {
// there was an error somewhere during route matching
res.status(500).send(err.message)
} else if (redirect) {
// we haven't talked about `onEnter` hooks on routes, but before a
// route is entered, it can redirect. Here we handle on the server.
res.redirect(redirect.pathname + redirect.search)
} else if (props) {
// if we got props then we matched a route and can render
const appHtml = renderToString(<RouterContext {...props}/>)
res.send(renderPage(appHtml))
} else {
// no errors, no redirect, we just didn't match anything
res.status(404).send('Not Found')
}
})
})
Server rendering is really new. There aren't really "best practices" yet, especially when it comes to data loading, so this tutorial is done, dropping you off at the bleeding edge.
Thanks for sticking with this tutorial all the way to the end!
If you want to learn more:
- Browse and run the examples
- Read the docs
- Read the source.
Happy routing!