8 Common Mistakes Made by Backbone.js Developers

Backbone.js is a lightweight framework designed to offer a basic toolkit for building structured web application front-ends. It provides components that create an intuitive development environment, especially for those familiar with back-end models and views. While simple, Backbone.js models and collections boast powerful features like easy integration with REST JSON APIs, yet remain adaptable for diverse use cases.

This tutorial examines typical errors encountered by freelance developers new to Backbone.js and offers solutions to sidestep them.

Mistake #1: Overlooking Backbone.js’s Feature Set

Despite its minimalism, Backbone.js, complemented by Underscore.js, offers a rich set of features catering to both fundamental and advanced web application development needs. A common pitfall for beginners is perceiving Backbone.js as just another MVC framework. While seemingly obvious, thoroughly exploring the framework is crucial. Its compactness makes this exploration manageable, especially given its small size and nicely annotated source code.

Backbone.js provides the essential structure for your web application. Its extensibility and plethora of plugins enable the creation of impressive web applications. Key features are accessible through models, collections, and views. Router and history components provide a streamlined mechanism for client-side routing. Although Underscore.js is a dependency, it’s deeply integrated, with models and collections leveraging this JavaScript utility belt.

The well-commented source code is concise enough to review over a cup of coffee. Beginners can gain significant insights into the framework’s internal workings and adopt a neat set of best-practices in JavaScript by examining the source annotations.

Mistake #2: Direct DOM Manipulation in Response to Events

A common habit for those new to Backbone.js is deviating from its recommended practices. For instance, handling events and view updates as one might with jQuery on basic websites. Backbone.js is about imposing a clear structure through separation of concerns. However, developers often update views directly in response to DOM events:

1
2
3
4
5
6
7
8
var AudioPlayerControls = Backbone.View.extend({
	events: {
		click .btn-play, .btn-pause: function(event) {
			$(event.target).toggleClass(btn-play btn-pause)
		}
	},
	// ...
})

This approach should be avoided. While obscure scenarios might justify it, there are typically superior alternatives. One such alternative involves using the model to represent the audio player’s state and rendering the button (specifically, its class names) based on this state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var AudioPlayerControls = Backbone.View.extend({
	events: {
		click .btn-play, .btn-pause: function(event) {
			this.model.set(playing, !this.model.get(playing))
		}
	},
	initialize: function() {
		this.listenTo(this.model, change, this.render)
		this.render()
	},
	// ...
})
1
<button class=”btn btn-<%- playing ? ‘pause’ : ‘play’ %>”></button>

While direct DOM manipulation within event handlers might occasionally make sense, the overhead associated with managing complex manipulations often outweighs the benefits. Backbone.js aims to address this. Using Backbone.js in this manner is counterproductive.

Mistake #3: Underestimating Rendering Costs

Backbone.js’s ease of DOM rendering and re-rendering can lead to overlooking its performance impact. There are multiple ways to overuse the render method, which might seem insignificant with increasingly performant browsers. However, as applications and data grow, performance degradation becomes evident.

Let’s illustrate this with a simple example of rendering a small model collection into a list view:

1
2
3
4
5
6
7
var AudioPlayerPlaylist = Backbone.View.extend({
	template: _.template(<ul> <% _.each(musics, function(m) { %> <li><%- m.title %></li> <% }) %> </ul>),
	initialize: function() {
		this.listenTo(this.collection, add, this.render)
	},
	// ...
})

Here, re-rendering occurs with every model added to the collection. This works fine initially. However, imagine fetching a large model set from the server. The render method, triggered for each model in the server response, will be invoked numerous times consecutively. A sufficiently large model set will cause noticeable stuttering, negatively impacting the user experience. Even small responses can become problematic depending on the view’s complexity.

A quick fix is to avoid calling the render method for every added model. When models are added in batches, you can use a technique to trigger the render method only once within a specific timeframe. Underscore.js, Backbone.js’s dependency, provides a useful function for this: “_.debounce”. Modify the event binding line as follows:

1
this.listenTo(this.collection, ‘add’, _.debounce(_.bind(this.render), 128))

The event callback will still fire for every “add” event, but the render method will be invoked only after 128 milliseconds have passed since the last event.

While often considered a quick fix, there are more suitable ways to mitigate render thrashing. The developers of Trello wrote a blog post their experience and techniques for optimizing rendering performance in Backbone.js.

Mistake #4: Leaving Event Listeners Bound Unnecessarily

Unbound event listeners are a common issue regardless of the JavaScript framework used. Backbone.js provides ways to avoid this, but neglecting them can lead to memory leaks. Backbone.js’s “Event” component allows for easy implementation of event-driven features. Views, being the primary event consumers, are prone to this mistake:

1
2
3
4
5
6
7
var AudioPlayerControl = Backbone.View.extend({
	initialize: function() {
		this.model.on(change, _.bind(this.render, this))
		// ...
	},
	// ...
})

The event binding here resembles the first example. We’ve replaced “this.listenTo(this.model, …)” with “this.model.on(…)”. The familiarity of “.on()” from other frameworks can lead to its overuse in Backbone.js. While acceptable, failing to call “.off()” to unbind event handlers when they’re no longer needed results in memory leaks.

Backbone.js offers a simple solution: the “object.listenTo()” method. It allows the object on which “listenTo()” is called to track its event listeners and easily unbind them all at once. For instance, views automatically stop listening to all events when “remove()” is called on them.

Mistake #5: Creating Oversized Views

Backbone.js’s minimalism allows for significant flexibility in front-end architecture. However, it’s crucial to keep models, collections, and views lightweight and focused. Views often become the heaviest aspect of an application code-wise. Avoid creating monolithic views that try to handle everything. Instead of a single “AudioPlayer” view, break it down into logical sub-views: one for the playlist, another for controls, one for visualization, and so on. The required granularity depends on the application’s complexity.

Granular views, each performing a specific task well, make Backbone.js development smoother. Code becomes more maintainable, extensible, and easier to modify. However, there is a balance to strike. Backbone.js views are designed for working with models and collections. Use this as a guide for structuring your application. Ian Storm Taylor shared some valuable ideas useful advice regarding view implementation on his blog.

Mistake #6: Limiting Backbone.js to RESTful APIs

Backbone.js works seamlessly with JSON-based RESTful APIs using jQuery or alternatives like Zepto. However, its extensibility allows for adaptation to other API types and encoding formats.

The “Sync” component handles front-end interaction with back-end services. Overriding its attributes allows customization of how Backbone.js communicates with API endpoints. You can even replace the default sync mechanism entirely, for example, using localStorage for data persistence instead of back-end services.

Numerous plugins simplify the customization of Backbone.js’s sync behavior. For instance, Backbone.dualStorage enables both back-end service and localStorage persistence. When offline, the plugin utilizes localStorage for cached data access and tracks changes for later synchronization with the server when online.

While RESTful back-ends offer ease of use with Backbone.js, it’s not a limitation. Modifying the default sync mechanism expands compatibility to various back-end APIs and encoding formats.

Furthermore, other Backbone.js components offer flexibility. The default Underscore.js templating engine can be replaced, and even the view component can be swapped for a different solution.

Mistake #7: Storing Data on Views Instead of Models

Beginners sometimes store data directly on views as attributes, often for tracking state or user selections. This practice should be avoided.

1
2
3
4
5
6
7
8
9
var AudioPlayerVisualizer = Backbone.View.extend({
	events: {
		click .btn-color: function(event) {
			this.colorHex = $(event.target).data(color-hex)
			this.render()
		}
	},
	// ...
})

You can create models and collections without endpoints to manage data that doesn’t require back-end persistence or is temporary. Storing this data in models allows for change tracking. Relevant views can observe these models and re-render as needed.

Storing state variables directly on views necessitates calling the render method after every change. Missing even a single call can lead to inconsistencies between the application’s state and the user interface. Synchronizing these variables across multiple views further complicates the process.

Mistake #8: Using jQuery “.on()” Instead of Delegated Events

Backbone.js provides an elegant way to handle DOM events that offers advantages over jQuery’s “.on()”. While “.on()” seems convenient, it can become cumbersome. For example, jQuery automatically removes event handlers bound with “.on()” when elements are detached from the DOM. This means any DOM events bound within a view need to be re-bound if the root element is detached and then re-attached.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var AudioPlayerControls = Backbone.View.extend({
	events: {
		click .btn-play, .btn-pause: function() { /* ... */ },
		click .btn-prev: function() { /* ... */ },
		click .btn-next: function() { /* ... */ },
		click .btn-shuffle: function() { /* ... */ },
		click .btn-repeat: function() { /* ... */ }
	},
	// ...
})

When the view’s element is re-attached, calling “delegateEvents()” on the view rebinds all events.

It’s important to note how these events are bound. Instead of binding directly to elements specified by the selector, Backbone.js binds the handler to the view’s root element. This works well in most scenarios. Changing child elements within the view’s DOM subtree doesn’t require rebinding events to new elements, as existing listeners continue to function.

However, this approach limits the listening scope for certain events, such as want to listen for scroll events on “window” on a scrollable child element. In such cases, a sub-view can be created for that element to handle its events.

Conclusion

Backbone.js, a compact yet highly extensible framework, is a great choice for web applications requiring flexibility. Unlike frameworks like Angular.js and Ember.js, which impose specific ways of doing things, Backbone.js provides powerful tools and empowers you to utilize them as needed. This tutorial highlights common development pitfalls and empowers you to leverage Backbone.js effectively for building outstanding applications.

Licensed under CC BY-NC-SA 4.0