Library like navigo or react-router are really great. But how do they work? Do we really need to import the entire library? Or is it ok to just use a 10% of it? In general, creating a fast and useful router is very simple and requires little less than 100 lines of code.

Requirements

Our router should be:

Written in es6+

Compatible with history and hash routing

A reusable library

Let’s start to code

Generally only one instance of router is used for each webapp but there are many cases where it is necessary to have more than one, for this we will not implement a Singleton pattern. Our router need 4 basic properties to work:

routes : The list of registered routes

: The list of registered routes mode : hash or history

: hash or history root : the root of the application if we are in history mode

: the root of the application if we are in history mode constructor: a base function to create a new instance of the our Router

class Router { routes = []; mode = null ; root = "/" ; constructor (options) { this .mode = window .history.pushState ? "history" : "hash" ; if (options.mode) this .mode = options.mode; if (options.root) this .root = options.root; } } export default Router;

Adding and removing routes

Adding or delete a route will just push or delete an element from an array

class Router { routes = []; mode = null ; root = "/" ; constructor (options) { this .mode = window .history.pushState ? "history" : "hash" ; if (options.mode) this .mode = options.mode; if (options.root) this .root = options.root; } add = ( path, cb ) => { this .routes.push({ path, cb }); return this ; }; remove = path => { for ( let i = 0 ; i < this .routes.length; i += 1 ) { if ( this .routes[i].path === path) { this .routes.slice(i, 1 ); return this ; } } return this ; }; flush = () => { this .routes = []; return this ; }; } export default Router;

Get the current path

We obviously need to know where we are in a specific moment in our application.

To do that we have to handle both cases, history mode and hash mode. In the first case we have to delete the root path from the window.location , in the latter we have to delete the “#” . We also need a function (clearSlash)to delete the / from the start or from the end of the string to not limit the developer in route writing phase

[...] clearSlashes = path => path .toString() .replace( /\/$/ , "" ) .replace( /^\// , "" ); getFragment = () => { let fragment = "" ; if ( this .mode === "history" ) { fragment = this .clearSlashes( decodeURI ( window .location.pathname + window .location.search)); fragment = fragment.replace( /\?(.*)$/ , "" ); fragment = this .root !== "/" ? fragment.replace( this .root, "" ) : fragment; } else { const match = window .location.href.match( /#(.*)$/ ); fragment = match ? match[ 1 ] : "" ; } return this .clearSlashes(fragment); }; } export default Router;

Navigate to

Ok, we have an API for adding and removing URLs. We are also able to get the current address. Next step will involve navigating to a route. Again, we are doing different things depending on the mode property.

[...] getFragment = () => { let fragment = "" ; if ( this .mode === "history" ) { fragment = this .clearSlashes( decodeURI ( window .location.pathname + window .location.search)); fragment = fragment.replace( /\?(.*)$/ , "" ); fragment = this .root !== "/" ? fragment.replace( this .root, "" ) : fragment; } else { const match = window .location.href.match( /#(.*)$/ ); fragment = match ? match[ 1 ] : "" ; } return this .clearSlashes(fragment); }; navigate = ( path = "" ) => { if ( this .mode === "history" ) { window .history.pushState( null , null , this .root + this .clearSlashes(path)); } else { window .location.href = ` ${ window .location.href.replace( /#(.*)$/ , "" )} # ${path} ` ; } return this ; }; } export default Router;

Listen for changes

Now we need a logic which will notify us for changes in the address bar. Every type of changes, like link or the .navigatemethod that we have created. We also need to be sure to render the right page at the first visit. We could use the popstateto listen to every change in the browser, but since we are studying the logic behind it, we will implement a simple setInterval.

class Router { routes = []; mode = null ; root = "/" ; constructor (options) { this .mode = window .history.pushState ? "history" : "hash" ; if (options.mode) this .mode = options.mode; if (options.root) this .root = options.root; this .listen(); } [...] listen = () => { clearInterval( this .interval); this .interval = setInterval( this .interval, 50 ); }; interval = () => { if ( this .current === this .getFragment()) return ; this .current = this .getFragment(); this .routes.some( route => { const match = this .current.match(route.path); if (match) { match.shift(); route.cb.apply({}, match); return match; } return false ; }); }; } export default Router;

Conclusion

Our library is over and ready to be used! Just **84 **lines of code for a complete Router. Obviously there is still a lot of logic that libraries that we find on github implement, but how many times has it happened to make a site that only needs to render two routes?

I’ve created a Github repo with all the code and a usage example. You can find it here:

thecreazy/create-a-modern-javascript-router