I’ve used React Router on most of the projects I’ve worked on over the last few years, and as I’ve dealt with different routing requirements, I’ve always been pleased to see that React Router could handle whatever I threw at it.
But this flexibility doesn’t come for free.
It’s kinda like buying a VCR that can do multi-timeslot recording, automated channel switching, and sports a menagerie of inputs and outputs. A deluxe piece of technology to be sure, but it also has 73 buttons on the remote.
Sometimes you just need a play button.
So, I decided to strip React Router out of one of my sites and implement something that was tailor made.
What you will find below is certainly nothing revolutionary, and if you’re an expert, there’s probably nothing in this post for you. But the adventure was a success and I found the process interesting.
Maybe it will be interesting to you too.
Sweetening the pot
Although I didn’t start this with the aim of improving performance, there was a rather staggering 20% drop in load time.
The package size dropped by 13% from 104 KB gzipped to 90 KB, so I would have expected a drop in download/parse time of about the same. I’m honestly not sure where the other 7% improvement came from, but I’ll take it.
Let’s get into it.
Step 0: Requirements gathering
My requirements are actually pretty simple:
- Based on the URL, render a particular page (represented by a component)
- When the user clicks a link, update the URL (without a full page refresh)
- When the URL updates, update the page
Step 1: Selecting a page based on the URL
The component below takes a URL (actually just the pathname part) as a prop, maps that to a component, and renders it. Couldn’t be simpler.
An aside… This is actually a great example of my new development paradigm: “don’t build for tomorrow what could be done tomorrow”.
You see, this structure isn’t well suited to route matching. And I can’t define actions here to assign to each route. And it’s in the same module as the
<App> component, so can’t be used elsewhere.
Should I arrange it now so that I can more easily add these features later if I want? Isn’t that good, future-proof code?
The correct question is: “will it be harder to do that in the future than it is to do today?” The answer is quite obviously no, so for today at least, my code will not be more complex that it needs to be.
Next up, I need to pass in the current pathname to this component, on both the server and in the browser.
Step 2: Server-side rendering
express on the server, so pathname is available in
req.url, which means I can render the page like so:
Really, that’s the whole thing. I hadn’t thought about this part until I got to it, and was quite pleased by how simple it was. (In a more serious environment I’d handle 404s with a
Okey dokey, that’s big bad server-side-rendering out of the way, on to …
Step 3: Client-side rendering
location.pathname in the browser matches
req.url on the server, everything just works.
At this point, the app is working for any given route and is pleasingly simple. Now I just need to be able to update that route in the browser …
Step 4: Updating the URL
There are two approaches to this particular step:
- Update the URL in one place. That is, treat
document.locationas the ‘state’ that holds the current route. Then listen for changes in
document.locationto update the page.
- Update the URL in two places. That is, store the ‘current route’ in an actual store and update both the URL and the store at the same time. No need to listen for changes to
Number two means either passing some
updateRoute() method in to every
<Link> or using React’s context. I dont know about you, but I have never used React’s context for something and not regretted it later (sage advice from Facebook: “If you want your application to be stable, don’t use context.”).
So, #1 it is.
I just need a component that updates the URL when a link is clicked. (Although not for external links, nor for links where the user wants a new tab.)
I’ll tell you all about
history.push() in the next section.
I can now use a link like:
<Link href="/about">About</Link> and trust that it will update the URL to
/about when clicked.
Bonus tip: always use a
<Link> component instead of anchor tags directly. Then check to see if you’re creating an
target="_blank". If you are, add
rel="noopener noreferrer" automatically. This is why.
The next step is to make sure the
<App> component re-renders when the URL changes …
Step 5: Reacting to URL changes
I had originally thought I’d just use good ol’ HTML5 History, but the history API has no concept of ‘listening’ to URL changes.
No problem though, I’ll just use the
history package that
Implementing this package was a little bit slippery because I had to call
listen on the same instance of
history as the one on which I called
So I needed a module that exported an instance of
history, and then I could rely on the fact that subsequent
imports of this module would return a cached version, and therefore the same instance. The module looked like this:
But hang on, if I need a module to wrap the history package so that I’m using the same instance, that means that under the hood they’re probably just registering event listeners so that when I call
push(), they call the callbacks registered with
Well shucks, I can do that myself.
There we go, saved myself 2.5 KB.
A brief performance intermission
You might think getting excited over 2.5 KB is silly, but the load time dropped by a decent 15%.
I’ll bet neither of us would have predicted that adding a little tiny package would add so much to the load time.
This is why I highly recommend keeping a spreadsheet handy to record these sorts of things. These are my scribblings for this exercise…
I log the time in
<App>. Then in Chrome DevTools, I tick Preserve log in the console settings, throttle the network and CPU, then just keep hitting refresh to the beat of Stayin’ Alive.
If I was to get all preachy I would suggest that if you don’t do something like this, your site is almost certainly slower than it needs to be.
End performance intermission.
Earlier I showed that I have an
<App> component that accepts a
pathname. On the server I pass it
req.url, in the browser I pass it
But now I want to update the pathname. So instead of using the prop directly, I will set the state to hold this pathname in the constructor.
When the URL changes, my listener will fire, being passed the new pathname, which I can use to update the state.
So now it looks like this:
Very simple routing without all the extra stuff.
If you’re interested, I tried to keep the change as simple as possible and did it in a single commit. But please, before you go and view the code, know that I originally wrote the site two years ago and thus contains none of the wisdom I have gathered over the last two years.
In other words, it’s shit and I know it.
Was it worth it?
I like to look back on experiments like this and ask myself if I would actually do this in a production environment in exchange for money.
The answer this time is an unnecessarily loud yes.
if statements, there is nothing ‘weird’ about this code — nothing a new developer would need time to decipher, and nothing that needs documenting more so than any other code.
Well this is embarrassing. I was going to add a bit here about parsing more complex paths like
/user/:id. I recalled that at some point in the past I’d read an article that mentioned this.
I went searching for it, found it, read through it, aaaand, it turns out I’ve pretty much ripped off the whole article.
So, um, prior art: You might not need React Router by Konstantin Tarkus. Oh wait, that guy’s the Kirasoft guy, maker of
react-starter-kit. Yeah don’t bother with my post, go read his instead. I should put this at the top…
Angry commenter appeaser section
I’ve noticed that I get the angriest comments on posts that could potentially be construed as “you’re doing it wrong” (and I’ve got a decent sample size).
I have grown to enjoy the ferocity of such remarks (“couldn’t code his way out of a wet paper bag” is my fav so far), but I figured I might preempt a few of the most likely misunderstandings:
- This is not an anti-react-router post. React Router does what it’s supposed to do well.
- Nor is it a my-solution vs their-solution competition. So no need to count and compare lines of code.
- “If what I have works, why would I go and write my own from scratch?” Settle down, nobody’s twisting your arm.
- “React Router has documentation, yours doesn’t.” Yeah, 27 pages of documentation. But we’ve already agreed that this isn’t a competition.
- “React Router was never meant to be used for such a paltry site.” Yeah, that’s what I’ve just come to realise.