Skip To Main Content

Communicating Between Views in Client-Side Apps

Posted by Rebecca Murphey

Jun 07 2012

I got to hear some great presentations and have some great conversations at last week’s inaugural Backbone Conf, and one thing that came up over and over again was how to effectively communicate between views in client-side applications. There are lots of patterns for doing this — and we love talking patterns at Bocoup — so I thought I’d go over a few.

For the sake of this conversation, we’ll talk about my favorite demo app, Srchr. When a user enters a search term into Srchr, three things happen:

  • the recent searches list is updated
  • the request is sent to the server
  • when the server responds, the results area is updated

A naive, non-MV*, jQuery-based way to solve this might look like:

mvc-less-srchr.js

$("#searchForm form").submit(function(e) {
  e.preventDefault();

  var term = $('#searchForm input').val(),
      req = $.getJSON('http://search.twitter.com/search.json?' +
            'callback=?&q=' + encodeURIComponent(term));

  $('#recentSearches').append('<li>' + term + '</li>');

  req.then(function(resp) {
    var resultsHTML = $.map(resp.results, function(r) {
      return '<li>' +
        '<p class="tweet">' + r.text + '</p>' +
        '<p class="username">' + r.from_user + '</p>' +
      '</li>';
    }).join('');

    $('#searchResults').html(resultsHTML);
  });
});

In an MV* land, we throw away this tightly coupled approach, and split our user interface into three distinct views:

  • the search form
  • the search results list
  • the recent searches list

Once we’ve split up our app into these three views, we need to reconnect them so that they interact with each other appropriately. The challenge is to do so in a way that doesn’t reintroduce the tight coupling we saw above. Below, we’ll go over a few of our options.

Pub/Sub

One option is to have views broadcast messages to the entire application when something interesting happens. For example, the search form could announce that a user had searched for something.

You’ll see this work in a couple of ways. In the Backbone Boilerplate, for example, there is an object named app that is extended with the Backbone.Events functionality. Views are given access to this object, and they trigger events on it; other views with access to the object can bind to events on it.

You can also use a pub/sub plugin to achieve the same effect.

So, for example, we might do the following in our search form view’s submit handler:

pubsub.js

onSubmit : function(e) {
  e.preventDefault();
  var term = $.trim(this.$('input').val());
  if (!term) { return; }
  this.app.trigger('search', term);
}

Then, anywhere else in our app, we could bind to the app object’s search event, and react accordingly. Generally, we’d give all of our views access to this app object.

Pub/sub is very much a “fire-and-forget” approach — we have no idea whether anything in our application responded to the announcement of the search. This may seem undesirable, but it actually makes unit testing a view incredibly straightforward: we can simply test that a view announced what it should have announced when it should have announced it. It also makes a view’s functionality easy to mock when we’re testing other pieces of code — rather than needing to set up an actual view, we can just trigger an event on the app object.

There are a few potential downsides of pub/sub, however:

  • There’s no built-in namespacing for your published “topics” — if another piece of your application starts using the search topic for a different meaning, your application is likely to start misbehaving, so it’s up to you to make sure there’s no accidental overlap.
  • You’ll need to take care that bindings to events on the app object (or to published topics in the more traditional pub/sub model) are properly torn down to avoid memory leaks.
  • The fire-and-forget nature of pub/sub can be more global than you’d like it to be — literally any piece of your application that has access to the app object can react to pub/sub announcements.
  • Debugging a system where anything can talk to anything can be painful — when things go wrong, the code that breaks can be far from the code that triggered the event.

Evented Views

The shortcomings of a pub/sub approach lead us to a more refined, less global option: evented views. In this pattern, rather than sending messages through an effectively global bus, we trigger events directly on our views; only code that has a reference to a view has an opportunity “hear” the announcement.

In this pattern, we’d start by setting up our views in our route code (or similar):

evented-views-route.js

'/search' : function() {
  var searchForm = new SearchForm();
  var recentSearches = new RecentSearchesCollection();
  var searchResults = new SearchResultsCollection();

  new SearchResults({ collection : searchResults });
  new RecentSearches({ collection : recentSearches });

  searchForm.on('search', function(term) {
    recentSearches.add({ term : term });
    searchResults.fetch({ add : true, data : { q : term }});
  });
}

Then we’d do the following in our search form view’s submit handler:

evented-views-view.js

onSubmit : function(e) {
  e.preventDefault();
  var term = $.trim(this.$('input').val());
  if (!term) { return; }
  this.trigger('search', term);
}

This approach lets us ensure that only code that has a reference to our search form can react to its announcements; it also essentially eliminates the namespacing issues of the pub/sub approach. Finally, our bindings to view events will get torn down when the view itself is destroyed.

How well this approach fits into your application will depend on your app’s overall structure and architecture — for example, if you aren’t setting up views in a consistent way, you’ll definitely find this difficult. You also run the risk of tying yourself in knots trying to make your views available in all the places you need them — if you find yourself doing that, it’s probably time to pause and consider a different approach.

Application Model

A third approach is to use one or more models to transmit messages between views by leveraging the events that are triggered when we get or set a property on a model. In this pattern, we’d give our views access to an application model, in addition to giving them the models they’ll need to display the appropriate data.

application-model-route.js

'/search' : function() {
  var recentSearches = new RecentSearchesCollection();
  var searchResults = new SearchResultsCollection();
  var applicationState = new Backbone.Model();

  new SearchForm({
    app : applicationState
  });

  new SearchResults({
    app : applicationState,
    collection : searchResults
  });

  new RecentSearches({
    app : applicationState,
    collection : recentSearches
  });
}

Our search form’s submit handler might then look like this:

application-model-view1.js

onSubmit : function(e) {
  e.preventDefault();
  var term = $.trim(this.$('input').val());
  if (!term) { return; }
  this.options.app.set('searchTerm', term);
}

Our search results view would be listening for the application model to change:

application-model-view2.js

initialize : function() {
  this.options.app.on('change:searchTerm', function(coll, term) {
    this.collection.fetch({ add : true, data : { q : term }});
  }, this);

  this.collection.on('add', function() {
    this.update();
  }, this);
}

You might also implement this approach with multiple models for representing different pieces of application state; for example, a User model for keeping track of user information, and a UI model for keeping track of UI state (such as whether a panel is open or closed). Sometimes these models will have a direct relationship to your server-side models, but more frequently, they’ll be used only for managing client-side state, and won’t have a server-side representation.

If we choose this path, we still have our unbinding problem — realistically, this is easily dealt with by adding some convenience methods to our view to help us with smarter binding (and I hear that this may become easier in a future version of Backbone) — but overall, this might be the most MV*-ish way of dealing with relationships between views.

How is this different from a pub/sub approach? In some ways, pub/sub is just a more generic version of the event system used by models, and our application model(s) are ultimately just another method of getting a message from one place to another. Unlike a simple pub/sub pass-through, however, a model can make decisions about when and whether to announce changes. For example, in Ember, related changes are batched and announced all at once, rather than one at a time, and a similar system can be implemented in other MV* frameworks as well. It’s also possible to instruct a model not to announce a change. These are all beneficial features, but they make models a bit less straghtforward to work with than pub/sub.

Which Approach is the Right Approach?

I find that I’m still partial to the second approach — evented views that localize the announcement of interesting view-related occurrences, without having a strong opinion about what those occurrences mean. The pub/sub approach is a bit too global for me in most cases, and while the application model approach is appropriate in many cases, it’s good not to fall into the habit of using models just because you feel like “MV*” says that’s what you’re supposed to do.

In all likelihood, though, your application will use some combination of these methods: perhaps pub/sub for occurrences of truly global interest, evented views for transferring messages within a page, and an application model for information that is meaningful across multiple “pages” of a single-page application.

There is one thing that I feel strongly that you should almost always avoid: views with direct knowledge of other views, unless those views have a clear parent-child relationship. When you tightly couple views, you effectively require that one cannot exist without the other. This increases your testing burden, and decreases reusability. In a parent-child situation, this might make sense, but in other cases, the downsides are abundant.

Finally, I think it remains to be seen whether a library can effectively generalize this portion of client-side app development. Making generalizations about models, routers, and even views is clearly possible, but good decisions about this portion of app development still involves some context-specific decision making, and I think we’re still a ways away from a consensus on the best approach. How are you solving communication between views?

Thanks to Mike and Greg for their detailed feedback; it made this post much better.

Comments

We moved off of Disqus for data privacy and consent concerns, and are currently searching for a new commenting tool.

Contact Us

We'd love to hear from you. Get in touch!