Get A Model From A Backbone Collection Without Knowing If The Collection Is Loaded

In working with a client recently, we ran in to a rather sticky situation. We were setting up a route to “persons/:id”, and as you would expect, we wanted to load the person in question and display the details of that person. The trick, though, is that we needed to wait until the persons collection was loaded to be able to retrieve the person from the collection. If we navigate to this route from somewhere else in the application, this isn’t an issue. The persons collection was already loaded and everything goes on normally. If we use a bookmark to get to this url directly, though, then there was no guarantee that the persons collection was loaded because we had not previously run any code to load the collection.

Depending on how the application is architected, and when the persons collection is expected to be loaded, there are a few options that I can see for solving this problem.

Option: Fetch The Model From The Server

The most basic option, and probably the easiest to deal with, is just to fetch the model from the server based on the id parameter that you get from the route.

In this case you just need to create a new model instance, set the `id` attribute on the model directly and then call `.fetch` on the model. It will make the trip to the server to get the data. You can either listen to a “change” event on the model or provide a “success” callback in the fetch method, to know when the data has been returned so that you can load it in to your view and display it as needed.

personById: function(id){
  var person = new Person();
  person.id = id;
  person.fetch({
    success: function(model, response){
      App.showPerson(model);
    }
  })
}

The major problem here is that you have not loaded the persons collection at all. If the persons collection is expected to be loaded because it’s being used for something related to displaying or working with the individual person model, then this option might not work for you. You could run some additional code to load the collection separately (asynchronously, since it works that way by default). This might help get around any potential issues of needing the collection.

Option: Use The Collection’s “reset” Event

Another easy option may be to load the collection when the request for the route is made. You would set up a new collection instance, bind a callback function to the collection’s “reset” event, and then call `.fetch` on the collection. The callback method would be responsible for retrieving the specific model from the collection and then creating and displaying the view.

personById: function(id){
  var persons = new PersonCollection();

  // note that "bind" is now "on" in Backbone v0.9.x
  persons.on("reset", function(collection, response){
    var person = collection.get(id);
    App.showPerson(person);
  });

  persons.fetch();
}

There are some potential issues with this solution, though. If you already have the persons collection loaded, then you’re going to load it a second time just to get the one model from it. To mitigate this problem, you would need two different entry points in to the display of the person: one for when you hit the route directly through a bookmark, and one for when the user is already in the app and navigates to the person display through other means.

Having two different entry points in to this part of the app may not be a bad idea. This largely depends on how the application is architected. You wouldn’t want to duplicate all of the code that sets up the display of the person’s details in both of the entry points, but you wouldn’t want to have a bunch of ugly if-statements in that code to determine how to set things up, either. Some simple abstractions of the common bits would help keep this code manageable.

Option: Building An “onReset” Callback

UPDATE:

FYI – I have an updated version of this code available in my Backbone.Marionette plugin, as Backbone.Marionette.Callbacks. It reduces the code and complexity significantly, and also eliminates the race condition issues that I mention below. Be sure to use that Callbacks object instead of the code I’ve listed here.

:END UPDATE


The third option that I can think of – and the one that I implemented for this particular client project – is a variation of using the collection’s reset event. The idea is to build an “onReset” callback system that is aware of whether or not the collection has already been loaded or is still waiting to be loaded.

If you have the persons collection being loaded from some other application initialization code, then you don’t necessarily have the ability to use a simple reset event as shown above. You could try to use the reset event, but there’s a race condition that is introduced in low-latency, high speed networks (i.e. your local development machine).

If you don’t control when the `.fetch` method is called, then you may end up binding to the reset event after the collection has already been reset. In that scenario – which is very likely to happen when working in a local development environment – your view for the specific person model will never get displayed.

The solution I came up with is to have a callback mechanism built in to the collection, that pays attention to the collection’s reset event and knows to either wait for the reset event to be fired, or to fire the callbacks immediately because the collection has already been loaded. I’m calling this an “onReset” callback, for lack of a better description at this point.

The code to use the onReset callbacks would look something like this:

// App.js
// ------
// some initialization code that happens elsewhere, using Backbone.Marionette

App.addInitializer(function(){
  App.persons = new PersonCollection();
  App.persons.fetch();
});



// PersonRouter.js
// ---------------
// The router callback that needs to get the person

personById: function(id){
  App.persons.onReset(function(collection){
    var person = collection.get(id);
    App.showPerson(person);
  });
}

In this setup, adding an onReset callback guarantees the callback’s execution. If the collection has not yet been loaded, then it stores the callback and waits for the reset event to fire. If the reset event has already been fired, then it simply executes the callback immediately. Either way, your callback will be executed and you will have the collection available when it does.

Race Condition Reduced. Eliminated?

Here’s the implementation for the onReset code. It’s generally functional and I haven’t yet run in to any problems, yet.

OnResetCollection = Backbone.Collection.extend({
  constructor: function(){
    var args = slice(arguments);
    Backbone.Collection.prototype.constructor.apply(this, args);

    this.onResetCallbacks = [];
    this.on("reset", this.collectionReset, this);
  },

  onReset: function(callback){
    this.onResetCallbacks.push(callback);
    this.collectionLoaded && this.fireResetCallbacks();
  },

  collectionReset: function(){
    if (!this.collectionLoaded) {
      this.collectionLoaded = true
    }
    this.fireResetCallbacks();
  },

  fireResetCallbacks: function(){
    var callback = this.onResetCallbacks.pop();
    if (callback){
      callback(this);
      this.fireResetCallbacks();
    }
  }
});

You can then extend from OnResetCollection instead of Backbone.Collection to get this functionality.

I still worry that there’s a potential race condition in between the logic and the pop’ing of items off the array. I’ve travelled every logical path of execution for the asynchronous call and onReset call that I can think of, and I can’t find any issue. I would love to hear from someone more experienced with race conditions in JavaScript, though.


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 Async, Backbone, Javascript, Marionette. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://twitter.com/ojohnnyo Johnny Oshika

    Thanks for this great post, Derrick.  That’s a great technique and I don’t think you’ll run into any race conditions. 

    It looks like we’re encountering similar problems building apps with Backbone.

    I took a different approach to solve this problem.  I’m using a cache object that manages the fetching and “state” of models and collections.  This cache object exposes something similar to getPersonById and getPeople methods, which immediately return a person model or a people collection whether they’ve been loaded or not.   Inside people or person, there’s a state property that signals to the view the current state of the object, which the view uses to render different things (e.g. loader animation, error message, or the loaded model).  It looks something like this: http://stackoverflow.com/a/8763213/188740

    I’m not sure how good this technique is, but it’s been working well for the most recent app that I built.