A React Router

Thomas Collardeau
5 min readJun 21, 2015

--

with Hasher

When I tried React-Router, I wasn’t too enthused. I probably should have spent more time with it, but it seemed like too many modules to import all over the app (Router, Route, Link). I had some problem with testing, having to do some hack around context. I understand React-Router 1.0 is much improved. But nonetheless, let’s create here our own router component in React, using the convenience of the Hasher library.

Photo by Julien Sister

What you say, Hasher?

What is Hasher? It’s a JS library that enables us to reliably use the history state of the browser and the hash value:

http://www.example.com/#about/

From the Hasher docs:

Hasher does other nifty tricks as well, check out the docs.

With Hasher, we can focus on React and not dwell on browser inconsistencies in regards to handling history and the url hash parsing.

A Component Sees the Light of Day

Our Router component will reign at the top of the app. It’s the entry point. Everything else flows down from it.

Let’s jump right in some code and explain what’s happening along the way. We’ll be using ES6 (I’ve also put the code on Github).

import React from 'react';
import hasher from 'hasher';
import App from './App.js';
class Router extends React.Component { constructor() {
super();
hasher.init();
this.state = { hash: ''};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
hasher.changed.add(this.handleChange);
hasher.initialized.add(this.handleChange);

}
render () {
return <App route={this.state.hash} >
}
handleChange() {
this.setState({
hash: hasher.getHash()
});
}
}
React.render(
<Router />,
document.getElementById(‘app’)
);

Hopefully, React and ES6 are self-evident. Let’s talk through the Hasher part:

  • In the constructor, we initialize Hasher to begin recording history changes.
  • On mount, we add a hash change listener, which will call our handleChange function when the hash changes. (This is really where the magic happens).
  • We also kick off an immediate change after initializing Hasher to respond to the initial value of the hash.
  • Finally, we pass the hash as a prop down to our App Component. (We will soon see that App.js will compose the routes).

Simple enough, right? Yet, it’s kind of magical.

Now, when the hash portion of the url changes (say the user navigates to ‘/#about’), handleChanges springs into action, and sets the hash state to the new value, so the component re-renders from the top down.

Kaboom. That’s it.

Hasher keeps track of the history so you can click back and forth and not be thrown off your SPA.

Routes, Where Do We Go

We still need to react (pun-intended) to the route props and compose our views as we please. We have a lot of flexibility. We could have a navigation component for one route and not for another. Let’s define 3 routes:

// in App.jsrender() {
switch(this.props.route) {
case “about”:
return (
<Header />
<About />
);
case “browse”:
return (
<Header />
<Browse />
<Footer />
);
default:
return (
<Header />
<Page404 />
<Footer />
);
}
}

That works quite well. It’s easy to read and re-arrange. Yet it’s still quite rudimentary. We have only one url token to play with. We need to handle parameters as well!

http://www.example/com/#/browse/item123/

We Need Params

Alright, that ought to be do-able. It’s just javascript, right? Let’s go back to Router.js and add a method to breakdown the hash part of the url using ‘/’ as a delimiter:

getHashInfo() {
let hash = hasher.getHash();
let parts = hash.split(‘/’);
return {
route: parts.shift(),
params: parts
};
}

This method will return an object with a route and a shiny new array of parameters. In our previous url example, getHasInfo would return :

{ route: 'browse', params:['item123']}

Hence, we have all the url information we need. Let’s update the code to make use of our new method on a hash change, and pass the data downstream:

handleChanges() {
this.setState({
hashInfo: this.getHashInfo()
});
}
render () {
return (
<App route = { this.state.hashInfo.route }
params = { this.state.hashInfo.params }
/>
);
}

Now, we also pass down the array of params down to the App component which itself can hand it down to whoever needs it. For instance:

//  in App.js case “browse”:
return (
<Header />
<Browse params={this.props.params} />
<Footer />
);

The Browse component can deal specifically with the parameter, possibly calling a flux action with it. It’s nice to know that, in any component, you can potentially reach for url information via this.props.

Reaping the Benefits

The nice things is that you deal with the router only at the top of the app, and you never see it anywhere else, it’s invisible, serving as (or at least resembling) a high-order component. We avoid mixin and any extra module imports down in our components. We can link urls normally with anchor tags. Moreover, we are harnessing the power of React, virtual DOM and one-way data flow.

Private Routes

We can add a bit more functionality to our Router. Let’s add some private routing.

let privateRoutes = [‘user', 'account'];
let isPrivateRoute = route => privateRoutes.some(view => view === route);

We introduce here an array to store our private routes, and a function to see if a given route is inside our array.

When the component updates (the hash has changed), we can see if the new route passes the test, and redirect the user if necessary.

componentWillUpdate() {
let hashInfo = hasher.getHashInfo(),
route = hashInfo.route;
if(isPrivateRoute(route) && authUtils.isLoggedOut()) {
hasher.setHash(‘login’);
}
}

I’ve sneakily added an authUtils module here as demonstration (else the private route would never be accessible). It determines if the user is logged out. If the route is private and the user is logged out, we divert to the login page using Hasher’s setHash().

Launch Pad

Implementing transition redirects is feasible, having hasher’s history to rely upon. Shall we not like having hashes in the urls, Crossroads.js will work with Hasher to modify the urls.

The code would likely get messy for apps with deeply nested routes, but many projects are well suited for a route one or two level deep, and a number of params.

Do you have ideas how to improve this code? Do you see some other limitations?

The boilerplate code of this demo up on github. Please feel free to give it a whirl and contribute any suggestions. :)

@collardeau

--

--