In my last post I demonstrated how you can define your site's routes using a data structure. The changes I made might seem trivial given the simple example that I used, so here I want to show you how we can make further use of the route list that we built previously.

You can find the example code for this post on GitHub and view it running on GitHub pages.

Let's start with a problem.

String Literals Everywhere

One of my issues with React Router is that it seems to encourage the user to hard-code string literals everywhere in their app. I've seen this in the wild several times:

// app.js const App = () => ( < BrowserRouter > < Route path = "/home" render = { HomeView } /> < Route path = "/about" render = { AboutView } /> < Route path = "/login" render = { LoginView } /> < Route path = "/register" render = { RegisterView } /> < Route path = "/user/:userId/profile" render = { ProfileView } /> </ BrowserRouter > ) // home.js - somewhere in render() if ( ! user . isLoggedIn ) { return < Redirect to = "/login" /> } else if ( user . hasProfile ) { return < Redirect to = { ` /user/ ${ user . id } /profile ` } /> } // header.js const Header = () => ( < header > < Link to = "/about" > About Us </ Link > < Link to = { ` /user/ ${ user . id } /profile ` }> About Us </ Link > </ header > )

This smattering of strings is fine until you want to change something. To update your routing you now have to hunt down all these path strings scattered around your code base and update them. Hope you don't make any typos!

Constants

The typical solution to this kind of problem is to define some shared constants so that you can avoid writing the same string over and over.

// consts.js const ROUTES = { HOME : '/home' , ABOUT : '/about' , LOGIN : '/login' , REGISTER : '/register' , USER : '/user/:userId/profile' , }

Now we can avoid hard coding strings when we write our links, routes and redirects:

// elsewhere.js const AboutUs = () => ( < Link to = { ROUTES . ABOUT }> About Us </ Link > )

This is much better, but there's still a lingering probem - what do we do about parameterised routes, like the user profile? We could just make the view code add the parameter:

// elsewhere.js const AboutUs = () => ( < Link to = { ROUTES . USER . replace ( ':userId' , userId )}> About Us </ Link > )

Alternatively, we could turn our path constants into functions:

// consts.js const ROUTES = { HOME : () => '/home' , ABOUT : () => '/about' , LOGIN : () => '/login' , REGISTER : () => '/register' , USER : userId => ` /user/ ${ userId } /profile ` } // elsewhere.js const AboutUs = () => ( < Link to = { ROUTES . USER ( userId )}> About Us </ Link > )

I think that this approach is pretty workable, and I have no problem stopping here. Still, we can do better by ensuring that our routing code relies on a single source of truth.

Green Code: What Do I Want?

We can break our implementation up into two parts:

green code: the nice, pretty interface that the user deals with

red code: the greasy, under-the-hood machinery that does the heavy lifting

Let's start by writing the green code. This is the code that the end-user (ie. you) will be working with day to day. It should ideally be simple, readable and easy to change.

Using the same technique as in my previous post, I'm going to define the API that I want before implementing it. What I want is something like Django's reverse function:

routes should have names that are decoupled from their path

path parameters can be passed in as data, with no need to mangle strings

all of the routing config is defined in a single source of truth

In my previous post, I defined all my site's routes in a single data structure:

const ROUTES = [ { path : '/home' , view : HomeView }, { path : '/about' , view : AboutView }, { path : '/login' , view : LoginView }, { path : '/register' , view : RegisterView }, { path : '/user/:userId/profile' , view : ProfileView }, // Add your next view here ]

Extending this, I want to be able to define a name for each route:

// routes.js const ROUTES = [ { name : 'HOME' , path : '/home' , view : HomeView }, { name : 'ABOUT' , path : '/about' , view : AboutView }, { name : 'LOGIN' , path : '/login' , view : LoginView }, { name : 'REGISTER' , path : '/register' , view : RegisterView }, { name : 'USER' , path : '/user/:userId/profile' , view : ProfileView }, ]

In addition, I want to refer to these routes by name in a declarative style:

// elsewhere.js const AboutUs = () => ( < NamedLink to = { ROUTE_NAMES . USER } params = {{ userId }}> About Us </ NamedLink > )

That's it! Now we need to make it happen.

Red Code: Making It Happen

Now that we've defined our green code code API, we need to write the red code that fulfils the promises that we've made.

I'm going to show you the implementation that I wrote for the API defined above, but it's not really the point of this post. The key takeaway is once you've written the green code, the red code just about writes itself.

We need to implement:

NamedLink

NamedRedirect

ROUTE_NAMES

I'll start with the link and redirect components, which mostly just wrap their React Router counterparts:

// Assume buildPath('/foo/:id/bar', { id: 1 }) => '/foo/1/bar' const NamedRedirect = ({ to , params = {}, ... args }) => ( < Redirect to = { buildPath ( to , params )} {... args } /> ) const NamedLink = ({ to , params = {}, ... args }) => ( < Link to = { buildPath ( to , params )} {... args } /> )

Next we can implement buildPath , based on the spec above. This is a bit yuck to read, but who cares? It's red code.

// ('/foo/:id/bar', { id: 1 }) => '/foo/1/bar' const buildPath = ( to , params = {}) => { let target = to for ( let [ key , val ] of Object . entries ( params )) { target = target . replace ( ` : ${ key } ` , val ) } return target }

Finally, we need to build the ROUTE_NAMES object out of our ROUTES list:

const ROUTE_NAMES = ROUTES . reduce (( obj , { path , name }) => ({ ... obj , [ name ]: path }), {}) console . log ( ROUTE_NAMES ) // ROUTE_NAMES = { // HOME: '/home', // ABOUT: '/about', // LOGIN: '/login', // REGISTER: '/register', // USER: '/user/:userId/profile', // }

Conclusion

That's it! We've now got a small and simple link and redirect API which:

keeps string literals out of our codebase

decouples our route paths from the views which refer to them

accepts path parameters with no string mangling

keeps all our route config in one place

can be extended further

You might, for example, want to implement a feature similar to React's PropTypes , where the buildPath function logs a warning when it encounters an invalid path. Alternatively, you could add a noRedirect flag to define routes which can never be redirected to. Either of these changes would be pretty easy to add on.

I really like this approach and have been using it a lot lately. Give it a try!