James is the editor of React Armory, and has been creating things with JavaScript for over 15 years.
Read more by JamesFollow @james_k_nelson on TwitterRouting is the process of choosing what to render based on the browser’s current URL.
Not all apps need to perform routing. For example, single-screen utilities and games will often not need a router at all. But if your app does have links and URLs, handling URL changes effectively is important for UX and accessibility.
React itself does not have a built-in routing solution. Because of this, there are a number of community projects that can handle it for you, including the popular react-router project.
These projects each have their benefits, but they also introduce dependencies. In particular, many rely on React Context – an experimental API that the React maintainers ask you to avoid.
Luckily, it is surprisingly simple to create a simple router that doesn’t rely on context at all. Component state and lifecycle are all you need to hook React up to the relevant browser APIs. This guide will take you through the process of building a simple context-free Router using these tools.
The simplest way to implement routing in a React application is with a top-level Component that chooses which content to render by URL. For example, a top-level TopScreen
component may decide what to render based on window.location.pathname:
function Menu() {
return (
<div>
<a href="/news">News</a>
<a href="/profile">Profile</a>
</div>
);
}
function TopScreen() {
const location = window.location;
let content;
if (location.pathname === '/news') {
content = <h1>News</h1>;
} else if (location.pathname === '/profile') {
content = <h1>Profile</h1>;
} else {
content = <h1>Not Found</h1>;
}
return (
<div>
<Menu />
<div>{content}</div>
</div>
);
}
But while this app will work, it has one major issue; it will reload the entire page every time a user clicks a link.
The HTML5 History API makes it possible to update the browser’s URL without causing a page reload. It also emits browser events when the user clicks Back or Forward. By using this API, you can write applications that respond faster and behave as expected when Back or Forward are clicked.
Single page applications will often make use of the following two parts of the History API:
window
object, which is emitted when Back or Forward are clickedThe HTML5 History API is not without issues.
Firstly, there are inconsistencies between how the different browsers use the popstate
event. Some versions of Chrome and Safari will emit an event on page load while other versions won’t.
Secondly, navigation events are only emitted when they’re caused by external stimuli such as the Back or Forward buttons. When the application itself updates the URL, nothing will happen. This means that the application will need to manually pass the new URL from the link that caused it to the top-level component that is handling routing.
Typically, these issues are solved by creating a separate object which manages access to the HTML5 History API. This object will do two things:
While it is certainly possible to create your own object to manage access to the History API, in this guide we’ll use the popular history package.
history
packageAssuming your project is using webpack or create-react-app, start by adding the history
package to your project:
npm install --save history
If you're not using webpack or create-react-app, you may want to reconsider whether your app even needs URL-based routes.
As the History API is global, our object will also need to be accessible everywhere. For example, we can create a separate file and export the object from it, so that any other modules can later import it.
import createBrowserHistory from 'history/createBrowserHistory';
const browserHistory = createBrowserHistory();
export default browserHistory;
This guide will use the following methods and properties from the browserHistory
object:
browserHistory.location
An object that contains the browser’s current location. Its shape is similar to the object available at window.location
:
{
pathname: '/friends',
query: '?page=1',
hash: '#top'
}
browserHistory.listen(callbackFunction)
Subscribes to history events, returning a function that will cancel the subscription when called.
While subscribed, the callback function will be called with the latest location
each time the user navigates. For example:
const unlisten = browserHistory.listen((location) => {
console.log(location.pathname);
});
browserHistory.push(location)
Adds a new entry to the browser history using window.history.pushState()
, then calls any listeners registered with browserHistory.listen()
You can view the documentation for the full browserHistory
object API at the history package’s page.
Link
componentBy default, HTML <a>
elements will always cause the page to reload when clicked. If you’d like to update the URL using the History API, you’ll need to cancel the default behavior and then implement an alternative.
You could do this by adding an onClick
handler to each individual <a>
element, but since most links within an app will share the same logic, React applications will often use a component to handle links. Instances of this component behave exactly like <a>
elements, except that instead of refreshing the page, they will update the URL in-place using browserHistory.push()
:
This Link object assumes the browserHistory
object is imported from the file defined earlier.
If you don’t want to rely on importing a global browserHistory
object, you could also pass the object through React context. It is safe to use context in this situation as there will only ever be a single browserHistory
object.
class Link extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
const isLeftClick = event.button === 0;
// The browser or OS will sometimes implement different behaviors when the
// user holds a modifier key while clicking
const isModified = event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
// An `<a>` element's `target` prop lets you specify that the link should
// be opened in a different window
const hasNoTarget = !this.props.target;
// We shouldn't replace the browser's default behavior in special cases
if (isLeftClick && !isModified && hasNoTarget) {
// Prevent the browser from navigating
event.preventDefault();
// Instead of a normal URL, browserHistory.push() expects an object with
// `pathname` and `search` properties
const pathname = this.props.href.split('?')[0];
const search = this.props.href.slice(pathname.length);
const location = {pathname, search};
// Update the browser URL and notify any listeners added
// via browserHistory.listen()
browserHistory.push(location);
}
}
render() {
return (
<a {...this.props} onClick={this.handleClick}>
{this.props.children}
</a>
);
}
}
Use <Link>
wherever you would normally use <a>
. For example, the Menu
component from the first example would use <Link>
:
function Menu() {
return (
<div>
<Link href="/news">News</Link>
<Link href="/profile">Profile</Link>
</div>
);
}
When the URL is updated by browserHistory.push()
, nothing will happen by default. In order to update the displayed content, you will need to listen for history events and take appropriate action.
Since your TopScreen
component decides what content to display, the current location will need to be available within that component’s render()
method. You can accomplish this by storing the current location directly on state
, using the following process:
I’ve assumed that you’ll store the location
property of this.state
. Of course, you can call it whatever you’d like.
browserHistory.location
.class TopScreen extends React.Component {
constructor(props) {
super(props);
// Set the initial location
this.state = {
location: browserHistory.location
};
}
componentDidMount() {
// Listen for navigation events only after the component is mounted,
// and save the latest location to component state.
//
// We do this here instead of in `constructor()`, as listening for
// navigation events only makes sense when the component is mounted in
// a browser -- not when it is being rendered server-side.
this.unsubscribe = browserHistory.listen(location => {
this.setState({location});
});
}
componentWillUnmount() {
// Ensure our listener does not keep calling `setState` after
// the component has been unmounted
this.unsubscribe();
}
render() {
const location = this.state.location;
let content;
if (location.pathname === '/news') {
content = <h1>News</h1>;
} else if (location.pathname === '/profile') {
content = <h1>Profile</h1>;
} else {
content = <h1>Not Found</h1>;
}
return (
<div>
<Menu />
<div>{content}</div>
</div>
);
}
}
If you listen for location changes directly within the component that uses them, the component will be tied to the browser, preventing you from re-using it in a server-side environment.
A more flexible approach is to manage the current location within a separate App
component, passing its value to the view component via props
.
class App extends React.Component {
constructor(props) {
super(props);
// Set the initial location
this.state = {
location: browserHistory.location
};
}
componentDidMount() {
// Listen for navigation events only after the component is mounted,
// and save the latest location to component state.
//
// We do this here instead of in `constructor()`, as listening for
// navigation events only makes sense when the component is mounted in
// a browser -- not when it is being rendered server-side.
this.unsubscribe = browserHistory.listen(location => {
this.setState({location});
});
}
componentWillUnmount() {
// Ensure our listener does not keep calling `setState` after
// the component has been unmounted
this.unsubscribe();
}
render() {
return <TopScreen location={this.state.location} />;
}
}
By passing location
via props
, your TopScreen
component can be implemented as a simple function.
function TopScreen({ location }) {
let content = <Menu />
if (location.pathname === '/news') {
content = <h1>News</h1>;
}
else if (location.pathname === '/profile') {
content = <h1>Profile</h1>;
}
else {
content = <h1>Not Found</h1>;
}
return (
<div>
<Menu />
<div>{content}</div>
</div>
);
}
Handling all of your routes within a single TopScreen
component works to a certain point. But as your app grows, the amount of conditional logic required will become unwieldy.
One solution to this is to match only part of a route, passing the remainder of the route onto a subsequent screen – and this will be covered in the next update to this guide, which will published mid-August. Join React Armory to make sure you don’t miss the update, which will add live examples and cover: