Cleanest way to destroy every Model in a Collection in Backbone?

I'm a bit late here, but I think this is a pretty succinct solution, too:

_.invoke(this.collection.toArray(), 'destroy');

I recently ran into this problem as well. It looks like you resolved it, but I think a more detailed explanation might also be useful for others that are wondering exactly why this is occurring.

So what's really happening?

Suppose we have a collection (library) of models (books).

For example:

console.log(library.models); // [object, object, object, object]

Now, lets go through and delete all the books using your initial approach:

library.each(function(model) {
  model.destroy();
});

each is an underscore method that's mixed into the Backbone collection. It uses the collections reference to its models (library.models) as a default argument for these various underscore collection methods. Okay, sure. That sounds reasonable.

Now, when model calls destroy, it triggers a "destroy" event on the collection as well, which will then remove its reference to the model. Inside remove, you'll notice this:

this.models.splice(index, 1);

If you're not familiar with splice, see the doc. If you are, you can might see why this is problematic.

Just to demonstrate:

var list = [1,2];
list.splice(0,1); // list is now [2]

This will then cause the each loop to skip elements because the its reference to the model objects via models is being modified dynamically!

Now, if you're using JavaScript < 1.6 then you may run into this error:

Uncaught TypeError: Cannot call method 'destroy' of undefined

This is because in the underscore implementation of each, it falls back on its own implementation if the native forEach is missing. It complains if you delete an element mid-iteration because it still tries to access non-existent elements.

If the native forEach did exist, then it would be used instead and you would not get an error at all!

Why? According to the doc:

If existing elements of the array are changed, or deleted, their value as passed to callback will be the value at the time forEach visits them; elements that are deleted are not visited.

So what's the solution?

Don't use collection.each if you're deleting models from the collection. Use a method that will allow you to work on a new array containing the references to the models. One way is to use the underscore clone method.

_.each(_.clone(collection.models), function(model) {
  model.destroy();
});

You could also use a good, ol'-fashioned pop destroy-in-place:

var model;

while (model = this.collection.first()) {
  model.destroy();
}

Tags:

Backbone.Js