Backbone.js Events
You wire a button to bump a counter, click it, and nothing moves. The handler runs, a console.log proves it, but the number on screen never changes. Nine times out of ten the fix is not more code; it is understanding how Backbone passes messages between objects. That message system is Backbone.Events, and it is the single most useful thing to understand in the whole library.
This guide is code-first. Every snippet runs in a browser console with Backbone and its Models loaded, and each section builds on the last. By the end you will know the three methods you use constantly, the safe way to subscribe, the events Backbone fires for you, and the two or three bugs that catch everyone.
What you'll learn
The events system, and why it's everywhere
Backbone.Events is a small mixin, not a class you instantiate. Backbone stirs it into Model, Collection, View, and Router, which is why all of them can already trigger and listen. You can stir it into your own objects too, and that one line gives any plain object a full publish/subscribe API.
on, off, and trigger, the three you'll use daily
on subscribes, trigger publishes, and off unsubscribes. Here is the whole loop on a bare object used as an app-wide event bus:
var bus = _.extend({}, Backbone.Events);
bus.on('user:login', function (name) {
console.log('Welcome, ' + name);
});
bus.trigger('user:login', 'Ada');
// => Welcome, Ada
The argument after the event name ('Ada') is handed to every listener. Call bus.off('user:login') and the greeting stops firing. That is the entire mechanism; everything else in this guide is a convenience built on top of it.
listenTo and stopListening: the leak-free way to subscribe
There is a second way to subscribe, and in a View it is almost always the right one. listenTo flips who remembers the subscription. With on, the object firing the event holds your callback; with listenTo, the listener keeps the bookkeeping, so it can drop everything later in one call.
Why listenTo beats on for views
// Fragile: the model keeps a reference to this.render forever
this.model.on('change', this.render, this);
// Safe: the view owns the subscription and can release it
this.listenTo(this.model, 'change', this.render);
The payoff comes at teardown. A View's remove() method calls stopListening() for you, so every binding made with listenTo disappears the moment the view leaves the screen. Bindings made with on survive, and that survival is exactly how memory leaks start, which is the subject of a later section.
Events you get for free
You do not have to invent events for the common cases; Backbone already fires them. A Model emits change (and a per-attribute change:name) whenever set alters data, plus request, sync, and error around server calls. A Collection emits add, remove, update, reset, and sort, and it re-broadcasts the events of every model inside it.
Reacting to one attribute with change:name
var user = new Backbone.Model({ name: 'Ada', plan: 'free' });
user.on('change:plan', function (model, value) {
console.log('Plan is now ' + value);
});
user.set('name', 'Grace'); // nothing logs, name isn't watched
user.set('plan', 'pro'); // => Plan is now pro
Listening to change:plan instead of the broad change keeps a handler from running on every unrelated edit. The handler also receives the model and an options object, so you always have the full context of what just happened.
Custom events: naming, data, and event maps
Your own events work exactly like the built-in ones. Trigger any name you like and pass as many arguments as you need; every listener receives them in order. A light naming convention, a namespace, a colon, then the action, keeps a growing app readable.
this.trigger('cart:item-added', product, this.items.length);
// listeners receive (product, count)
Binding many handlers with an event map
When one object cares about several events on another, pass an object instead of repeating listenTo:
this.listenTo(this.collection, {
add: this.onAdd,
remove: this.onRemove,
reset: this.render
});
One caveat worth committing to memory: a View's events hash ({ 'click .save': 'onSave' }) looks similar but is a different system, it delegates browser DOM events, not the object events covered here. Keep the two mental models separate and a whole category of confusion goes away.
The bugs everyone hits
Almost every Backbone events bug is one of three things: a listener that never got cleaned up, the same handler bound twice, or this pointing at the wrong object. The first is the famous one.
The zombie view leak (and the one-line fix)
A "zombie" is a view you removed from the page that is still alive in memory because a model is holding its callback. Re-render a list a few times and you get stacks of invisible views all reacting to the same change. Binding with listenTo and tearing down with remove() prevents it:
var Row = Backbone.View.extend({
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () { /* draw the row */ return this; }
});
// when the row leaves the screen:
row.remove(); // removes the element AND calls stopListening()
Why this pattern outlived Backbone
The events system is Backbone's take on the observer pattern, and that pattern did not retire when Backbone did. Node's EventEmitter, the DOM's own addEventListener, and the reactive cores of React and Vue are all variations on the same theme: state changes in one place, and anything that cares is notified without the two sides knowing about each other.
From Backbone events to modern reactive state
When a modern framework re-renders a component because a piece of state changed, it is doing what model.set plus a change listener does here, just automatically and at finer granularity. Learn the pattern in Backbone, where it is explicit and you can watch every step, and the "magic" in newer tools stops being magic.
That is the whole events system: a publisher, some listeners, and the discipline to clean up after yourself. With it in hand, the rest of the Foundations guides, views, templates, and sync, are mostly about which events to listen for.
Frequently Asked Questions
What is the difference between on and listenTo in Backbone?
With on, the object that fires the event keeps a reference to your callback, so you must remember to call off yourself. With listenTo, the listener records the subscription, so calling stopListening (which a View's remove does automatically) cleans up every binding at once. Prefer listenTo inside Views.
How do I stop a Backbone view from leaking memory?
Bind with this.listenTo(model, 'change', this.render) instead of model.on(...), and remove the view with view.remove(). remove() detaches the element and calls stopListening, so the model no longer holds a reference to a view that is gone.
Can I pass data when triggering a custom Backbone event?
Yes. Any arguments after the event name in trigger are passed straight to every handler, for example object.trigger('user:login', user, Date.now()). Built-in events also pass data: a model change handler receives the model and an options object.
Are Backbone events the same as DOM events?
No. Backbone events are an in-memory pub/sub system between JavaScript objects and do not bubble through the DOM. A View's events hash does map to DOM events like click, but that is delegation on top of jQuery, separate from the object-level on/trigger system.
Read next: Backbone.js View Lifecycle, the next foundations guide, or What is a View? for the overview.
Keep building your foundation
Events tie the Backbone core together. See how the pieces fit in the complete guide to Models, Views, Routers, and Collections.
Explore the Backbone Guide →