Tips For Using Backbone.js Routers With HTML5 PushState

IMPORTANT UPDATE!

Jeremy Ashkenas pointed out that this won’t work in Internet Explorer (or other browsers that don’t support PushState). I had tested this, but apparently I didn’t hit the Back button in IE, in my testing. The Back button, indeed, does not work in IE when setting things up this way. So, if you don’t care that IE users can’t use their Back button, this works great… otherwise… I think I need to re-work some of this and post a followup with corrections.

 

Update #2

To avoid further confusion, I’m striking through everything I said that is wrong, in this post. A new post will be up soon-ish, to provide some real tips.

 

———————–

I’ve already introduced HTML5 PushState and talked about using progressive enhancement with Backbone.js to make it all work. What I haven’t talked about, though, is how I got my Backbone router to work properly after enabling PushState. I spent some long hours fighting with my router all because of a few very small decisions I originally made in my app. Hopefully these tips will help other avoid the same mistakes I made.

Tip #1: Don’t Use Backbone.Router

You don’t need a Backbone.Router if you’re using PushState. That’s pretty much the end of it. The rest of the “tips” I was originally going to write are pretty much useless because you don’t need to use a router when you’re using HTML5′s PushState.

WHAT?! How Am I Supposed To … ?!

Here’s the secret: The Backbone.Router doesn’t give you much functionality. Most of what we use a router for is done through the Backbone.History object. Router.navigate? How about History.navigate instead? Router.navigate delegates to this anyways. Route callback methods? Yeah, those are also delegated to History. Routers give us a nice way to organize our route definitions and callbacks in a clean way – and that’s a very important role to play – but end up delegating the majority of their functionality to the History object, anyways.

So, why not use the History object directly if we don’t need routes and callback methods?

Two Strings Attached: PushState And Navigate

There are two strings attached to this. The first is that you need to use PushState and use it properly. Secondly,

 

you need to stop using `router.navigate` to fire your route methods.

 

The PushState Requirement

Without PushState, you need a router to use hash fragments. Without a router, your hash fragments won’t be able to fire callback methods. Of course you could built your routes into the Backbone.History object directly (once again, Backbone.Router delegates to this). But it gets a little more complicated when you do this yourself. It’s easier to use a router and makes more sense to another developer looking at your code.

If you are using PushState, though, then you’ll never fire a router’s route methods. If you’re never firing a router’s route methods, what’s the point of having a router?

The Navigate Requirement

If you don’t have a router, you won’t be able to call `router.navigate(“…”, true)` with that pesky ‘true’ parameter. But, this shouldn’t be an issue, anyways, You should be building your apps in a stateful manner with state-based workflow instead of using Backbone as if it were a stateless web server. You’ll still want to call `history.navigate(“…”)` to update your browser’s URL. This is done in response to the application being put into a specific state, and not done to put the application into a specific state. Don’t pass the `true` parameter as the second argument to navigate, and you’ll be fine.

Sample Code

Here’s a standard router implementation, not using PushState, with two routes that can be fired from two separate links on the page.

MyRouter = Backbone.Router.extend({
    routes: {
        "foo": "foo",
        "bar": "bar"
    },
    
    foo: function(){
        $("#output").append("foo!<br/>");
    },
    
    bar: function(){
        $("#output").append("bar!<br/>");
    }
});

new MyRouter();
Backbone.history.start();


<a href="#foo">Say "foo"</a><br/>
<a href="#bar">Say "bar"</a><br/>
<div id="output"></div>

When you click on a link, the route is fired and a message is printed out on the screen. Simple stuff.

Enabling PushState

Now look at the code with PushState enabled.

MyRouter = Backbone.Router.extend({
    routes: {
        "foo": "foo",
        "bar": "bar"
    },
    
    foo: function(){
        $("#output").append("foo!<br/>");
    },
    
    bar: function(){
        $("#output").append("bar!<br/>");
    }
});

new MyRouter();
Backbone.history.start({pushState: true});


<a href="/foo">Say "foo"</a><br/>
<a href="/bar">Say "bar"</a><br/>
<div id="output"></div>

The only differences in this version of the code are the two links and the use of `{pushState: true}` when starting the router. The two links are standard links without hash fragments. This means that they would make a request back to the server when you click on them. If you run this code and click on a link, it makes a full request back to the server to render the page because the links are full URLs.

When the page loads from the server, you would expect the Backbone.Router to fire it’s route method, but it doesn’t. Backbone routers only fire route methods in two scenarios: 1) when a hash fragment is used as a route, and 2) when you call `navigate` with the second parameter of `true`. Since neither of these criteria are met, the router does not fire it’s route. If a router’s methods never fire, why do you have a router in your app? Delete it.

Hijacking The Links And Using History.navigate

To make use of Backbone’s capabilities with PushState turned on, and to update the URL without making a full request back to the server, we need to hijack the link clicks with a bit of JavaScript code and then use Backbone’s History object to update the URL.

MyView = Backbone.View.extend({
  events: {
    "click a.foo": "foo",
    "click a.bar": "bar"
  },

  foo: function(e){
    e.preventDefault();
    history.navigate("foo");
    $("#output").append("foo");
  },

  bar: function(e){
    e.preventDefault();
    history.navigate("bar");
    $("#output").append("bar");
  }
});

new MyView({el: $("body")});

history = new Backbone.History();
history.start({pushState: true});


<a href="/foo" class="foo">Say "foo"</a><br/>
<a href="/bar" class="bar">Say "bar"</a><br/>
<div id="output"></div>

The browser URL updates when you click on a link, but the page does not have to do a full refresh from the server. Of course, if you decide to hit the refresh button on your browser, you’ll hit the full URL and the server will give you the page you expect.

Conclusion: If You’re Using PushState, You Don’t Need A Router

Check out the JavaScript for my BackboneTraining.net site. There are no routers in site. There is only an instance of Backbone.History and a call being made to the history.navigate method to update the URL when a link is clicked. This works because I have PushState enabled and because I am not passing the `true` argument to the `navigate` method.

Given these two assumptions – PushState and not passing ‘true’ to ‘navigate’ – Backbone Routers become far less important to our applications. And I believe this is a good thing as it leads us away from using Backbone as if it were a stateless web app, and toward using Backbone as it truly is – a stateful application framework running on top of a stateless, asynchronous technology stack.



Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Derick Bailey

Derick Bailey is an entrepreneur, problem solver (and creator? :P ), software developer, screecaster, writer, blogger, speaker and technology leader in central Texas (north of Austin). He runs SignalLeaf.com - the amazingly awesome podcast audio hosting service that everyone should be using, and WatchMeCode.net where he throws down the JavaScript gauntlets to get you up to speed. He has been a professional software developer since the late 90's, and has been writing code since the late 80's. Find me on twitter: @derickbailey, @mutedsolutions, @backbonejsclass Find me on the web: SignalLeaf, WatchMeCode, Kendo UI blog, MarionetteJS, My Github profile, On Google+.
This entry was posted in Backbone, HTML5, Javascript, PushState. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Anonymous

    I just thought about dropping my router after this post, however I’m actually making use of their only benefit: extracting parameters from URLs, which is quite a nice but probably its only feature :D

  • http://nieve.heroku.com Nieve

    I probably got it all wrong, but when I go to your BackboneTraining.net site and I navigate through it, hitting back/forward doesn’t do anything- the view stays the same. I tried Firefox 7, Opera, the lastest chrome and nada. Is this the expected behaviour?

    • http://mutedsolutions.com Derick Bailey

      sadly, no… i didn’t intend that behavior, and now i see how mistaken i was when i wrote that code and this article. :-/

      I’ve posted a note at the top of this article saying I was wrong… but i need to do something more than that, i think.

  • http://mutedsolutions.com Derick Bailey

    FYI – ignore everything i’ve struck through in this post. turns out it’s garbage and as Nieve points out in the comments, doesn’t work the way I though it did.

  • sam

    Derick, did you ever write a follow up to this thread? Curious to see if you got rid of the router or not.

    • http://mutedsolutions.com Derick Bailey

      only in the notes at the top of the post… yes, you do need a router. it doesn’t work to get rid of them, for various reasons, including what’s listed here and because it often makes the most sense to have multiple routers in a single app.

  • http://andrewhenderson.me/ Andrew Henderson

    Trying to understand how Backbone was intended to be used with PushState. My app works, but when I add {pushState: true} to history.start it then only loads the initial request correctly.

    If the initial request route leads with a # it loads the route and removes the hash from the address bar. Any subsequent navigation however no longer works. None of the links that worked before “#foo” for instance, still work.

    Am I supposed to route manually when using PushState using router.navigate(“foo”, {trigger: true});?

  • Travis Wimer

    “I’m striking through everything I said that is wrong”, you then go on to strike through everything except half a sentence. I laughed.