React and pushState: You’re doing it wrong

In the world of React.js single-page apps, there are two types of routing: push-state, and hash-based. Each of these have their strengths and weaknesses.

Now as you may know, most of the React ecosystem focuses on push-state routing, using the HTML5 History API and (sometimes) server-side rendering with Node.js. There are a number of reasons for this, the two main ones being:

  • The HTML5 History API makes it possible to have multiple routes indexed by Google
  • Server-side rendering gives your app a few milliseconds faster load time on the first page load

In contrast, hash-based routing works by only considering the part of the URL starting with the # character, which I’ll refer to as the hash. This vastly simplifies things for a few reasons:

  • The server never sees the hash, meaning it is handled 100% in-browser
  • Changes to the hash do not by default result in page reloads
  • Buttons and links are just plain old javascript-free <a> tags

So let me just put this out there:

In most cases, designing your new app with push-state routing is a mistake.

Why?

Firstly, Most apps are hidden behind a login screen, and thus have at most one page which needs to be indexed by Google. This means that the only benefit you can possibly receive from push-state is the performance improvement. And before you ask – no, removing #/ from your page URLs is not a benefit.

Secondly, unless you’re Facebook, you probably shouldn’t be optimising for a few milliseconds speedup.

And thirdly, unless you write your entire backend with server-side rendering in mind, you won’t get that performance improvement anyway.

Maybe you’re an exception to these rules – but if you’re not, push-state routing adds enormous complexity for little to no benefit. In contrast, a roll-your-own hash-based router can be implemented in 23 lines of React:

// Renders the application for the given route
var Application = React.createClass({
  render: function() {
    switch (this.props.location[0])  {
    case '':
      return <div><h1>Index Page</h1></div>;

    default:
      return <div><h1>Not Found</h1></div>;
    }
  }
});

// Split location into `/` separated parts, then render `Application` with it
function handleNewHash() {
  var location = window.location.hash.replace(/^#\/?|\/$/g, '').split('/');
  var application = <Application location={location} />;
  ReactDOM.render(application, document.getElementById('react-app'));
}

// Handle the initial route and browser navigation events
handleNewHash()
window.addEventListener('hashchange', handleNewHash, false);

But James, you say – this looks much more complex than routing with react-router! Well, sure – it takes a few more lines. But you’re kidding yourself if you think that 31.37kb of minified code – which uses undocumented React features like contexts – is less complex than the above snippet.

No don’t get me wrong – push-state routing does have its place. However, using it where it isn’t needed is effectively gold-plating your app – wasting precious resources on unneeded features when what you really want is to just get the job done.

So next time you’re designing a new app, don’t shoot yourself in the foot; design your routing based on requirements, not trends. But what if you’re not confident enough to implement a hash-based router for your app? As it happens, you’re in luck! I’ll soon be releasing my guide to hash-based routing with Raw React. Get on my newsletter now to make sure you don’t miss it.

Update: Building a Router with Raw React has been released.

In return for your e-mail address, you’ll also immediately receive 3 print-optimised PDF cheatsheets – on React, ES6 and JavaScript promises.

I will send you useful articles, cheatsheets and code.

I won't send you useless inbox filler. No spam, ever.
Unsubscribe at any time.

One more thing – I love hearing from readers. If you have something to say, send @james_k_nelson a tweet, or send me an e-mail at james@jamesknelson.com. Thanks for reading!

Read More

8 Comments React and pushState: You’re doing it wrong

  1. Matt Ruby

    Thanks for writing this! I dealt with routing a few month’s ago and I came to the same conclusion. I still don’t understand why everyone goes with push-state out of the gate. It’s simply not needed.

    Reply
  2. Tony

    I humbly disagree. The hash tag in the URL is a barrier to a true hypermedia service that can exist in a browser just as it would in a under-the-hood http client using the API.

    Plus, having the full address means seeing the address as the referrer, which means server access logs and error logs go from an endless sea of “https://blah.com/index.php” to “https://blah.com/cars/xyz” (“hmm, seems like a lot of incoming requests are for that one record. That’s interesting. Oh look! A picture of boobs!” Or “you say the page fails to load ‘randomly’? Oh hey, looks like errors always trace back to these addresses! Always records where there is a special character in the title! Thank goodness I wasn’t depending on you to notice!”)

    I agree that using the history API with expectations of huge performance boosts is probably unrealistic. But there are benefits similar to SEO that aren’t actually dealing with search engine indexing. Having the data mappable and visible at both the server level and client level is *not* a bad thing.

    Reply
    1. James K Nelson

      Yep, pushState is a better option if you need your site to be crawlable. That said, most apps don’t actually have to be crawlable.

      Reply
  3. Jon Wyatt

    “Server-side rendering gives your app a few milliseconds faster load time on the first page load”

    This is an understatement, the increase in speed depends on network latency, number of data fetches etc and can often run into seconds on slow / mobile networks. Page load speed isn’t a luxury – it directly impacts on user’s impressions of the quality of a site, and has been shown to directly correlate with user bounce rate by Amazon.

    Reply

Leave a Reply

Your email address will not be published.