Using React Router To Match Arbitrary URLs (alternate title: Handling Wildcard URLs with React Router)
The motivation
I'm currently in the process of writing a wiki, and I'm using React on the front-end of my application.
Now, I wanted my wiki to be flexible enough that users could create new pages, then have a dedicated URL that goes straight to that page in the future.
For example, let's say I create a new page on my wiki called "Table of Contents". I wanted to be able to navigate to www.mywiki.com/table-of-contents
to see the contents of that page.
This meant that my React code on the front-end needed to be able to correctly handle any path and render the correct page. Luckily, React Router makes this possible.
The introduction
Ok, this tutorial assumes that you have a very basic familiarity with React, the web, etc. Honestly, if you clicked on this page, there's a 99.9% chance you have the prerequisites for understanding or at least implementing this overview.
Unlike some of my other tutorials, first I'm going to show you the code, then I'm going to break it down. Worst case scenario, you should be able to copy/paste this code to get things working.
The code
One thing to note before we dive in: I am using Browserify to bundle up all my front-end requirements (like the React library, the React Router library, etc). That's why you'll see the lines like var React = require('react')
.
Browserify is an awesome tool, but it is NOT required. If you don't understand those 2 first lines, simply take them out, but make sure you're including the React and ReactRouter libraries manually in your main html file.
This code will simply render a page that prints out the current HTML path.
Without JSX:
// Again, these two lines are because I'm using browserify
// If you're NOT using browserify, ignore these and
// add these libraries in your HTML using <script> tags
var React = require('react');
var Router = require('react-router');
// Rename Router.Route for convenience
var Route = Router.Route;
// Create the parent App component
var App = React.createClass({
displayName: 'App',
contextTypes: {
router: React.PropTypes.func
},
render: function() {
return React.DOM.div(
{},
this.context.router.getCurrentPath()
);
}
});
// Create a Route component that passes
// through to our App component
var routes = React.createElement(Route, {
name: 'app',
handler: App,
path: '*'
});
// Render the element
Router.run(routes, Router.HistoryLocation, function(Handler) {
React.render(
React.createElement(Handler),
document.body
);
});
With JSX (and with less comments):
var React = require('react');
var Router = require('react-router');
var Route = Router.Route;
// Create the parent App component
var App = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
render: function() {
return (
<div>{this.context.router.getCurrentPath()}</div>
);
}
});
// Create a Route component that passes
// through to our App component
var routes = (
<Route name="app" handler={App} path="*" />
);
// Render the element
Router.run(routes, Router.HistoryLocation, function (Handler) {
React.render(<Handler/>, document.body);
});
Here's what's happening in this code:
- We create a react class called "App". The important parts are:
- contextTypes: This part is necessary to get access to the "router" property that ReactRouter automatically added to the context.
- this.context.router.getCurrentPath(): This part just uses the
router
object that ReactRouter has made available, and calls thegetCurrentPath
method, which returns the path of the page.- So, if this code runs when the URL in your location bar is
www.simplewiki.com/table-of-contents
, thenthis.context.router.getPath()
will return "/table-of-contents" - Once you have this path, you can pass it down to any child components you want as a
prop
. Then, in that child component, you can query the database based on that page name, display the page name, or do whatever else you want. - End result: your page can now render any page it wants to, because it always knows the path of the page it is currently on.
- So, if this code runs when the URL in your location bar is
We create a new
Route
component (this is a component that we get for free fromReactRouter
), and pass in 3props
to this component:name
,app
, andpath
.handler
should point to yourApp
component, as this is how ReactRouter knows what component to renderpath
= "*" basically just means: whenever ANY path is seen, use thishandler
name
: not so important. Just put it in.
Finally, we set up the Router, and tell it to render the
Handler
component for whatever path it gets and to attach the result todocument.body
.- In the previous step, we said
path="*"
, so ANY path will use the handler we specified. Since we saidhandler=App
, it will render theApp
component todocument.body
. Router.HistoryLocation
is optional here.- If you include it, ReactRouter will handle full URLs, like
www.simplewiki.com/table-of-contents
. - If not, ReactRouter will handle
#
URLS, likewww.simplewiki.com/#/table-of-contents
- If you include it, ReactRouter will handle full URLs, like
- In the previous step, we said
Be careful with Router.HistoryLocation
If you choose to use Router.HistoryLocation
, you will need to set up your back-end to have a way to handle incoming requests to arbitrary URLs.
For example, in my application, any requests to ANY url that matches /*
(eg. www.mysite.com/table-of-contents
or www.mysite.com/another-page
) ALL get handled by the same endpoint, which simply returns my main index.html
page, which has something similar to the React code we outlined above.
This is because when anyone goes to your website, it first asks your web site server for information, THEN your server sends back the HTML, CSS, and JavaScript code (actually, this happens in a couple steps, but this is more or less accurate). The point is that if your server throws an error or doesn't return the right HTML/CSS/React code when someone types in an unknown URL, it won't matter that your front-end code is awesome, because the client will never see it.
TL; DR: It may be easier to NOT use Router.HistoryLocation
, at least at first.
Conclusion
This tutorial hopefully shows you how to use ReactRouter to handle wildcard (arbitrary) URLs, and how to determine what URL the user has selected inside your React code. What you do with that information (ie. whether or not you render the page differently) is up to you.
(Though feel free to post questions in the comments section--I'm happy to help out).
Edit (8/3/15)
I decided to refactor my Wiki recently, and it quickly became apparent that there are cleaner, better ways to approach using React Router for this purpose (especially in the new 1.0 beta version...but I'm using version 0.13.3). The above still works great, but here's another way to do the same thing:
Instead of having a single route, you could define your routes like this (note that I'm using JSX in this example):
var routes = (
<Route handler={App}>
<DefaultRoute handler={HomePage} />
<Route path="pages/:pageName" handler={WikiPage} />
</Route>
);
In the above example, if someone navigates to your application, React router will render the App
component, then allow you to specify a special component called RouteHandler
, which will represent the WikiPage
component if the url matches pages/:pageName
(eg. www.yourapp.com/pages/this-is-a-url
) and the Routehandler
will represent the HomePage
component if the url matches just www.yourapp.com
.
Here's an example of what the App
component code might look like if you use this approach:
var React = require('react');
var Router = require('react-router');
var RouteHandler = Router.RouteHandler;
// This is just a "Navigation Bar" component
var Nav = require('../Nav/Nav.jsx');
var App = React.createClass({
contextTypes: {
router: React.PropTypes.func.isRequired
},
render: function() {
var params = this.context.router.getCurrentParams();
return (
<div className="app">
<Nav />
<RouteHandler {...params} />
</div>
);
}
});
module.exports = App;
In the above example, React Router will render the App component for any matched route, so it will render a <div className="app">
that contains a Nav
bar and, depending on the URL, either a HomePage
or WikiPage
component.
params
in the above example will automatically take the value of any special parameters defined in the routes code. So, in this example, if I were to navigate to www.yourapp.com/pages/this-is-a-page-url
, which you can see matches our defined route: pages/:pageName
, then params.pageName
will equal this-is-a-page-url
.
Final note: the {...params}
syntax simply means: pass all params
that React Router found to the RouteHandler
component as props
.