Don’t Execute A Backbone.js Route Handler From Your Code
I was working on a sample Backbone.js application and I ran into a scenario that seemed like it should have been simple on the surface, but was causing me a tremendous amount of headache. Here’s the basic functionality that I was trying to achieve:
- Have a list of items displayed on the screen
- When an item is clicked:
- the item is highlighted
- the application routes to the item’s view url
This sounds like a fairly trivial list of requirements, right? I thought so, too, until I started got down into the weeds of the implementation. For the sake of this blog post, let’s say the application is a … blog. The list of items is a list of blog posts, and clicking on a blog post will highlight the post title in the list, and then display the contents of the post, etc.
A Quick Note On Event-Driven Architecture
I’m a follower of the philosophy that a backbone.js application is an event-driven application that responds to changes in the state of our models. That is to say we don’t want to have our views manipulating themselves and referencing / controlling each other directly. Rather, we want to have our views call methods on our models that manipulate the state of our models. In response to the state of our models changing, our application does things. This could be updating some visual element, routing to a new hash fragment, and/or anything else we can do in a web page.
To facilitate this philosophy, I use a lot of events in my application design – but not just model events. I rely heavily on application level events, too. Before you go on, it’s worth your time to b familiar with the use of an event aggregator to facilitate application level events. I’ve blogged about using an event aggregator with backbone.js in the past, and you should read that first, if you haven’t done so already.
The First Implementation
In the first implementation of my requirements, I implemented the links to the blog posts not as hash fragment links directly, but as events handled by a view, which tell the model it was “selected”, triggering a “post:selected” application event which is handled by a method that called router.navigate. At a high level, here’s what that code looks like (I’ve left out a lot of detail from this code, to only show the important parts).
From what I’ve seen, this code is an example of how a lot of people are using a router to navigate directly to a url fragment instead of having an link that points directly to the hash fragment. Sure, I’m adding a few layers of indirection with my events in order to support the philosophy I want, but the end result is the same: click a link that is handled by a backbone view and (eventually) the router is triggered, to navigate to the right place.
Honestly, there are a lot of problems with this code and with the idea of having a view call router.navigate in order to fire off the route’s handler method and display something (even if the router is called through a few layers of indirection). I could talk about coupling, encapsulation and other issues. However, there’s one very important functional problem, from the user’s perspective, that I want to focus on.
When the user clicks a post in the list, the click causes the model to become selected (line 35) which highlights the item in the list (line 22, 25-30). It also causes the router to navigate to a url hash fragment that will display the post by retrieving it from the list of posts and displaying it (line 4, 12, 55, 44). However, when a user navigates directly to the post url hash fragment by pasting it into the browser’s location, clicking a link from somewhere else on the web, or otherwise heading directly to it, the “selected” code is never fired.
You might think that it would be easy to fix this, like I thought. You can just add “post.select()” to line 46 (right after “var post = …” in the “showPost” method of the router) and everything will work, right? This will cause the model to be selected which will highlight the post in the list of posts. I did this at first, and I thought it was working. But there’s another set of problems that arise because of this.
When the router fires the showPost method for the first time, calling the model’s “select” method will eventually cause the event aggregator to fire the “post:selected” event which will then call the router.navigate to try and navigate to this url hash fragment again. Fortunately for us, the router is smart enough to know that it’s already sitting on that url hash fragment and it won’t fire the route’s handler again.
In addition, when we click the post from the list, having the router.navigate method called to set the url hash fragment and execute the router handler, we are duplicating some things in our process. First, the post is already loaded up in memory – we clicked on it after all (this may not be an issue in some systems, though. Perhaps it was only a PostPreview model that was bound to the list). Secondly, the post was already selected. Firing the route handler causes the post to be selected again, even though it’s already selected. If the application has some visual styling or behavior that is fired when the post is selected – my application slides things in / out – then you end up with duplicated effects and strange visual issues.
Of course, the end result is what we want – we have a highlighted post in the list and we have the post being displayed. However, I’m not happy with what we had to do in order to get to this point. I’m sure the users won’t be happy either, when they see the double-slide-in visual issue caused by the post being selected twice. Yes, there are some workarounds for the double-select, such as ensuring the post can only be selected if it’s not currently selected. However, this sort of “gate” is only a workaround and not really an elegant design or solution.
A good design and implementation, in my mind, would avoid these types of problems entirely. We wouldn’t have to put in checks to make sure we’re only selecting an item that isn’t currently selected. We wouldn’t have to worry about calling select twice because our app wouldn’t do that. We wouldn’t have to worry about strange doubled-up visual effects, or hitting a route twice, any of the other issues that i’ve describe, or any of the other issues such as coupling and encapsulation that I haven’t described. This, however, is not a good design.
The “AHA!” Moment Regarding Router.Navigate’s Second Argument
The documentation for backbone describes the second argument for the router.navigate method as a way to trigger or not trigger the route’s handler method. In the past (and very recently) I’ve been really frustrated by the default of this second argument being “false”. That is, if you do not pass a second parameter to router.navigate, the url hash fragment and browser history will be modified but the route’s handler method will not be called. If you want the handler method to be called, you must pass true as the second parameter.
Why?! When would you ever want to call router.navigate and not have it call the route’s handler method?! And then it hit me like a brick wall that had been standing there the whole time, only I wasn’t paying attention and didn’t see it until I had already smacked into it: most of the problems that I was having were caused by the eventual call to the router.navigate, passing true as the second argument.
A Better Implementation
I decided to try out a different implementation, then. Rather then have my application eventually call the router.navigate in order to fire the route’s handler method, I would instead have my application respond to the change in state in order to show the selected post. Then I could call router.navigate without the second parameter. This would update the url’s hash fragment and browser history, but it wouldn’t fire the route handler and I would avoid a lot of the problems that I was currently working around. I also need to allow the router to still work when a user gets a link from somewhere else, or types the url w/ hash fragment directly into their browser, etc. If I was going to facilitate both of these scenarios, then I would need a common chunk of code that could be called from either entry point.
Armed with this idea, I came up with a new implementation that looked more like this:
Notice that there are very few changes in this implementation. We’ve introduced a BlogController object – which, by the way, does not extend any backbone class because it does not need to – that removes the logic to display a post from the router (good encapsulation / single responsibility, etc). We’re no longer passing true as a second argument to the router.navigate function, in our handler for the “post:selected” event. And, the show post route’s handler method is not directly calling any view logic. Instead, the router is simply setting the state of the post by calling the post.select() method. From there, the standard logic to respond to the application’s state change kicks in, and the post is displayed.
Now, I know that calling “pos.select()” from within the route’s handler will still cause the router.navigate to fire. Once again, though, the router is smart enough to not do anything since we’re telling it to navigate to the hash fragment that we’re already on. This code isn’t “perfect”… but it’s still far better than what we started with.
Learning A Lesson About Router.Navigate
A router serves two purposes in a backbone app. The first purpose is to be a clean entry point for urls with hash fragments that are entered directly into the address bar of a browser, or clicked as links from external sites. The second is to manage the history of the browser, as your application moves between pages. Don’t mix these two things together. You shouldn’t be executing the route’s handler from within your application, most of the time.
Sure, you will need to call router.navigate to set the url fragment to an appropriate value at times, but you should listen to the defaulted second argument value and ask yourself if you really need to override this or not.
Don’t Fire Route Handler Methods From Within Your App
In all but the smallest of sample applications and special cases where there simply is no alternative that could cause the app to function correctly, I say the default value of “false” is the correct value for the second argument of router.navigate. By removing the “true” argument from the call to router.navigate, I was forced to find a different way to enable my application’s functionality. In this case, the introduction of a “controller” object (which has been greatly simplified for this blog post) allowed me to keep my code well encapsulated and provide a single logic path for the selection and display of a post.