Software, life, and random thoughts
Home/Categories/Backbone/3 Tips For Writing Better Backbone Views/

3 Tips For Writing Better Backbone Views

backbone One of the good things about Backbone.js is it doesn’t tell us how to do things. It leaves it for us to decide what are the best practices for writing views (or any components). This is also one of the worst things about Backbone. it makes it almost too easy to take the wrong path, and write views that will be hard to maintain. The principle we should follow when writing a view is to keep it encapsulated, and keep it as “dumb” as possible - a view should know only the bare minimum it needs to know in order to do it’s job, and do bare minimum it has to do.

Here are 3 simple tips that can help us achieve this:

1. Don’t Access Variables Out Of The View Scope

var View = Backbone.View.extend({
  initialize: function () {
    this.model = window.App.myModel
    this.model.fetch()
    //...
  }
})

While its tempting to write a view such as the above, it introduces some problems. The view needs to know about the window.App and it manipulates a model’s state. We have tight coupling. It violates the single responsibility principle: the view should be responsible only to render itself, but when it accesses the global scope, finds a model and operates a fetch on it, it becomes responsible for application state. Writing a unit test can quickly show us how coupled it is: we will need to mock window.App, add a mock model to it, make sure it fetches, etc.

A better pattern would be to take care of these in the views parent (controller/route)

var myView = new View({ model: window.App.myModel })
window.App.myModel.fetch()

In this example, we are passing a fully baked model, leaving only the rendering job to the view.

2. Write The Template Inline

There are three base techniques for writing and storing backbone/underscore templates:

1. In our main html file, in a script tag:
<script type="text/template" id="pod">
  <h2><%= title %></h2>
  <p><%= body %></p>
</script>
<script src="view.js"></script>
// view.js
var View = Backbone.View.extend({
  template: _.template($('#pod').html())
})

By using the script tag, we are telling the browser to not render this element. By choosing a script type that doesn’t have a matching interpreter, we ensure this script will not run.

2. In an external file, loading over network, with dependency management utilities such as requireJS
<%= title %> <%= body %>
// view.js
define(['text!templates/pod.html'], function (pod) {
  var View = Backbone.View.extend({
    template: _.template(pod)
  })
})
3. In our view declaration, as a string.
// view.js
var View = Backbone.View.extend({
  template: _.template(`
    <%= title %>
    <%= body %>
  `)
})

Option #1 has tight coupling between our view and the main DOM, and I wouldn’t recommend using it. Option #2 and #3 are both good. I tend to prefer #3 for a few reasons:

  • Its simple. No magic is happening.
  • Everything is in one file, which makes it easy to develop
  • No editor highlights html in a javascript string, so we are “forced” to write short and clean templates.

3. Render UI Upon Data Change, Not User Interaction

The flow is : User interaction -> data change -> view render. For instance, if we are writing a play/pause toggle button in an audio player, the flow would be:

  1. The user hits ‘play’
  2. The model (data) state changes to ‘playing’
  3. The view renders in ‘playing’ mode

Following this pattern ensures that changes to the state triggered from other sources (such as fast forward, new playlist, network error, etc.) will toggle our button to do the right thing. In other words, we have a single source of truth: whenever our model changes we know we should render our button.

The code can look like:

var PlayPauseButton = Backbone.View.extend({
  tagName: 'li',
  className: 'icon',
  initialize: function () {
    this.model.on('change:status', this.render, this)
  },
  render: function () {
    this.$el.removeClass('icon-play').removeClass('icon-pause')
    this.$el.addClass('icon-' + (this.isPlaying() ? 'pause' : 'play'))
  },
  events: {
    click: function () {
      this.model.set('status', this.isPlaying() ? 'paused' : 'playing')
    }
  },
  isPlaying: function () {
    return this.moedl.get('status') === 'playing'
  }
})

Conclusions

Keeping decoupled and encapsulated views helps us writing a sane, maintainable application.
Following some best practices makes it very easy to do so. We all used the lack of time as an excuse to explain the poor quality of our code (at least I did), but in most cases, writing good code doesn’t take more time.

©2020 Uzi Kilon