33

I have read in several places that calling the Backbone.history.navigate function is considered bad practice.

For example Addy Osmani sais in his book "Developing Backbone.js Applications"

It is also possible for Router.navigate() to trigger the route along with updating the URL fragment by passing the trigger:true option. Note: This usage is discouraged...

http://addyosmani.github.io/backbone-fundamentals/#backbone.history

Or Derick Bailey in his blog post even sais:

You shouldn’t be executing the route’s handler from within your application, most of the time.

But I don't really understand the reasoning behind it and what would be a better solution.

In my opinion it is not really bad to call the navigate function with the trigger:true option. The route function could upon calling always check if the considered data is already loaded and show this loaded data instead of doing the whole work all over again...

DanEEStar
  • 6,140
  • 6
  • 37
  • 52
  • {trigger: true} is just another tool. Used in the right places, it's a valuable piece of code. Think, for example, if you $.ajax some information around before navigating a person to #/userProfile/:userId. There's no other way of doing it besides using window.navigate, which is arguably worse. Besides that, it does allow users to bookmark where they've been sent to.. or refresh the page and get their state back (assuming your app supports it). So don't just throw {trigger: true} into the same bucket as eval. :) – Stephen Jul 22 '13 at 22:00
  • @Stephen I have to disagree with you, discouraging use of some tool means that when you want to use it, you should really ask yourself if there is not a better way of doing things. – mor Jul 22 '13 at 23:27

4 Answers4

25

There seems to be some confusion about what Router#navigate does exactly, I think.

Without any options set it will update the URL to the fragment provided. E.g. router.navigate('todo/4/edit') will update the URL to #todo/4 AND will create a browser history entry for that URL. No route handlers are run.

However, setting trigger:true will update the URL, but it will also run the handler that was specified for that route (In Addy's example it will call the routers editTodo function) and create a browser history entry.

When passing replace:true the url will be updated, no handler will be called, but it will NOT create a browser history entry.

Then, what I think the answer is:

the reason why the usage of trigger:true is discouraged is simple, navigating from application state to application state to application state requires most of the time different code to be run than when navigating to a specific application state directly.

Let's say you have states A, B and C in your application. But state B builds upon state A and state C builds upon B. In that case when you navigate from B to C only a specific part of code will need to be executed, while when hitting state C directly will probably execute some state checking and preparation:

  • has that data been loaded? If not, load it.
  • is the user logged in? If not redirect.

etc.

Let's take an example: State A (#list) shows a list of songs. State B (#login) is about user authentication and state C (#list/edit) allows for editing of the list of songs.

So, when the user lands on state A the list of songs is loaded and stored in a collection. He clicks on a login-button and is redirected to a login form. He successfully authenticates and is redirected back to the song list, but this time with delete-buttons next to the songs.

He bookmarks the last state (#list/edit).

Now, what needs to happen when the user clicks on the bookmark a few days later? The application needs to load the songs, needs to verify the user is (still) logged in and react accordingly, stuff that in the state transition flow had already been done in the other states.

Now for a note of my own:

I'd never recommend the above approach in a real application as in the example. You should check whether the collection is loaded when going from B to C and not just assume it already is. Likewise you should check whether the user really is logged in. It's just an example.

IMO the router really is a special kind of view (think about it, it displays application state and translates user input into application state/events) and should always be treated as such. You should never ever rely on the router to transition between states, but rather let the router reflect the state transitions.

Creynders
  • 4,573
  • 20
  • 21
  • 1
    Unfortunately you're right - I went through the 'ol code this morning and found that I was barking up the wrong tree. I've deleted my "answer" as it's misleading. – Stephen Jul 23 '13 at 15:31
  • This answer makes a lot of sense. As a follow up: Who is doing all this coordination work? Has the data been loaded, is the user logged in etc.. Is this some kind of a controller object? And couldn't this controller object coordinate the work of the router and the transitioning of the application states? – DanEEStar Jul 24 '13 at 07:00
  • Yeah, in small apps I use mediators to translate router events to method calls (or to trigger commands) and the other way around to translate system events to Router#navigate calls. In larger applications (or if the logic becomes more complex in smaller apps) I use finite state machines. – Creynders Jul 24 '13 at 07:08
  • 1
    @Creynders your answer contradicts itself as soon as you say you would never recommend the above approach in a real application. in both situations (in a "real application"), you have to do the checks to make sure the collection is loaded and the user is logged in. – Owen Masback Jan 15 '14 at 19:16
  • 1
    That's what I meant: you _should_ do the checks in both scenarios, i.e. for sake of the example I pretend there's a difference, but in reality there's not (at least not regarding collection and authentication checking) – Creynders Jan 16 '14 at 08:49
  • This still doesn't make sense. Your argument essentially boils down into making sure constructors have the full initialization code for the objects state. When do you not have to check for that? OO isn't a *pure* paradigm. We routinely keep state on objects and have to be mindful of this. And, in stateless-actions like /logout, /login, `{trigger:true}` would always be safe. – Evan Carroll Feb 24 '15 at 16:26
  • Ok, let me rephrase/add this: it's about _not_ relying on a single entry point for application state manipulation, since depending on the current application state different strategies may need to be applied and _if_ you do rely on a single entry point, you'll have to have a bunch of centralized strategy resolution logic. While when you don't rely on a single entry point the strategy resolution is distributed, encapsulated and therefore more DRY, clean and SRP. BTW how are logins/outs stateless? Well, the actions themselves are stateless obviously, but they _do_ modify application state. – Creynders Feb 25 '15 at 08:33
11

I have to disagree with @Stephen's answer here. And the main reason why is because the use of router.navigate({trigger : true}) gives the router responsibility to handle the application's state. It should only reflect application state, not control it.

Also, it is not a View's responsibility to change the hash of the window, this is the router's only job! Don't take it away from it! Good modularity and separation of concerns makes for a scalable and maintainable application.

Forwarding a person to a new section within your application

Backbone is an event driven framework, use events to communicate. There is absolutely no need to call router.navigate({ trigger : true }) since functionality should not be in the router. Here is an example of how I use the router and I think promotes good modularity and separation of concerns.

var Router = Backbone.Router.extend({
  initialize: function(app) {
    this.app = app;
  },
  routes: {
    'videoLibrary' : function() { this.app.videoLibrary(); }
  }
});

var Application = _.extend({}, Backbone.Events, {
  initialize: function() {
    this.router = new Router( this );
    this.listenTo( Backbone, 'video:uploaded', function() { 
      this.router.navigate('/videoLibrary');
      this.videoLibrary();
    });
  },
  videoLibrary: function() {
    //do useful stuff
  }
});

var uploadView = Backbone.View.extend({
  //...
  uploadVideo: function() {
    $.ajax({
      //...
      success: function() { Backbone.trigger('video:uploaded'); }
    });
  }
});

Your view does not need or want to know what to do when the user is done uploading, this is somebody else's responsibility. In this example, the router is just an entry point for the application's functionality, an event generated by the uploadView is another. The router always reflects the application state through hash changes and history but does not implement any functionality.

Testability

By separating concerns, you are enhancing the testability of your application. It's easy to have a spy on Backbone.trigger and make sure the view is working properly. It's less easy to mock a router.

Modules management

Also, if you use some module management like AMD or CommonJS, you will have to pass around the router's instance everywhere in the application in order to call it. Thus having close coupling in your application an this is not something you want.

chchrist
  • 18,854
  • 11
  • 48
  • 82
mor
  • 2,313
  • 18
  • 28
  • Excuse me, but when did I say that it was the view doing this? Don't jump to conclusions. A router can be in control of many views at a time. Beyond that, the reasoning behind using trigger isn't to force a router to respond - it's to ensure that the user of the system is able to get back to the page, or at minimum use their traditional navigation should they so choose. In any system, the focus should be on the end user - that's what trigger allows you to do, while being more testable than window.location. – Stephen Jul 23 '13 at 03:39
  • @Stephen You are right, I changed the phrase a couple times while writing and did not realize it came to cite you in a wrongful way. Sorry about that, I will edit, but I still disagree with your answer. – mor Jul 23 '13 at 13:39
  • I see what you're doing. I don't see how it's different or what you're gaining. Now you have to have the global `Backbone.trigger` which is just a more generic `Backbone.Navigate(stuff, {trigger: true})`. And, even worse, what you're listening to is just manually doing this `this.router.navigate('/videoLibrary');` anyway.. It seems you've just rewritten a router using Backbone.Events. – Evan Carroll Feb 24 '15 at 21:43
  • @EvanCarroll The example is pretty simple. The main point is for separation of concerns, the router should not trigger a state change, but reflect the changes. The Global `Backbone.trigger` could be any event bus extending `Backbone.Events` and do much more than just change the url hash. It's just a simple example to illustrate the mechanism. – mor Feb 25 '15 at 15:50
3

In my opinion it's considered bad practice because you should imagine a Backbone application not like a Ruby On Rails application but rather like a Desktop application.

When I say RoR, I'm just saying a framework supporting routing in sense that a route brings you to a specific call to the controller to run a specific action (imagine a CRUD operation).

Backbone.history is intended just as a bookmark for the user so he can, for example, save a specific url, and run it again later. In this case he will find the same situation he left before.

When you say:

In my opinion it is not really bad to call the navigate function with the trigger:true option. The route function could upon calling always check if the considered data is already loaded and show this loaded data instead of doing the whole work all over again...

That to me sounds smelly. If you are triggering a route and you are checking for the data to see if you have it, it means that you actually already had them so you should change your view accordingly without loading again the entire DOM with the same data.

That said trigger:true is there so do we have reason use it? In my opinion it is possible to use it if you are completely swapping a view.

Let's say I have an application with two tabs, one allows me to create a single resource, the other one let me see the list of the created resources. In the second tabs you are actually loading a Collection so data is different between the two. In this case I would use trigger:true.

That said I've been using Backbone for 2 weeks so I'm pretty new to this world but to me it sounds reasonable to discourage the use of this option.

dierre
  • 7,140
  • 12
  • 75
  • 120
1

It depends on your context.

If you have done something in your current view that might affect the view you are about to navigate to, for example creating for deleting a customer record, then setting trigger to true is the right thing to do.

Think about it. If you delete a customer record don't to want to refresh the list of customers to reflect that deletion?

l3x
  • 30,760
  • 1
  • 55
  • 36