Straightening our Backbone: A lesson in event-driven UI development

If you're interested in what we work on, please apply - we're hiring: http://mixpanel.com/jobs/

Mixpanel’s web UI is built out of small pieces. Our Unix-inspired development philosophy favors the integration of lightweight, independent apps and components instead of the monolithic mega-app approach still common in web development. Explicit rather than implicit, direct rather than abstract, simple rather than magical: with these in-house programming ideals, it’s little surprise that we continue to build Single-Page Applications (SPAs) with Backbone.js, the no-nonsense progenitor of many heavier, more opinionated frameworks of recent years.

On an architectural level, the choice to use Backbone encourages classic Model-View designs in which control flow and communication between UI components is channeled through events, without the more opaque declarative abstraction layers of frameworks such as Angular. Backbone’s greatest strengths, however – its simplicity and flexibility – are a double-edged sword: without dictating One True Way to architect an application, the library leaves developers to find their own path. Common patterns and best practices, such as wiring up Views to listen for change events on their Models and re-render themselves, remain closer to suggestions than standard practices, and Backbone apps can descend into anarchy when they grow in scope without careful design decisions.

A problem of communication

Our front-end apps, as they’ve grown organically, have at times become dangerously convoluted – a danger known to many JavaScript apps. A fair amount of the recent discussion concerning front-end frameworks has revolved around issues of understanding and controlling data flow. Facebook’s Flux architecture, for instance, builds explicitly upon the principle of “unidirectional data flow,” constraining the propagation of UI events and other data to specific paths between objects such as a Dispatcher, Stores, and Views (see “What is the Flux Application Architecture?” for a friendly high-level overview). As Henrik Joreteg understands it, “conceptually FLUX is the same as having an intelligently evented model layer in something like Ampersand or Backbone and turning all user actions and server data updates into changes to that state.”

Behind Joreteg’s statement lies an unwelcome truth: a thoughtless eventing setup leads to bad control flow issues. In the case of Mixpanel, as some of our front-end reports grew in scope and complexity, patterns which worked at first became unwieldy. Consider the following event/listener/state tangle:

event-soup

A click on a dropdown (1) makes the component update a UI state object (2). Listeners in both the dropdown and the Model layer register a change in the state object (3) and update themselves. The Model re-fetches data from a server; when the response comes back (asynchronously), it updates the state object with new graph data (4). The graph widget registers this change (5) and consequently updates the state object again after its internal updates (6), with a long chain of potential further repercussions to other listeners. This type of event-driven communication via a global state object (a single model changed by and listened to by all Views and Models as a sort of event bus) may work for very simple UIs, but for more involved interactions with more components and subcomponents produces “event spaghetti” with the typical problems of global variables: non-obvious mutual dependencies and code which is difficult to reason about. The symptoms of this underlying issue show up in the form of circular event loops (A triggers B triggers A triggers B etc.), unexpected double-renders, and subtle race conditions during pageload and bootstrapping an app.

These sorts of issues will not be unfamiliar to anyone who’s worked with many JS apps and frameworks. It’s par for the course with growing client-side apps, and at this point some would jump ship, seeking the clearer waters promised by more “magical” frameworks. As the recent pushback from the React/Flux camp has demonstrated, though, often what you need isn’t a monolithic abstraction layer so much as a simplified and explicit flow of data through your application. For us, the answer wasn’t to dump Backbone in favor of a shinier object, but instead to address the real issues within our apps – issues of our own creation – with a saner architecture.

Rearchitecting the front end

The primary mechanism by which we decoupled our UI components and simplified control flow is a classic pub/sub implementation. Components have no knowledge of the larger context in which they are embedded, but signal actions by emitting events which other objects can listen to independently. A sample interaction flow might look as follows:

mediator

In the above case, a user-initiated interaction (selecting an item through a subview) causes the GraphSelector component to emit an event notifying any interested subscribers that an item has been selected. A “mediator” picks up on the event and interacts proactively with other objects: it tells the model layer to fetch fresh data from the server, and informs another UI widget of state changes, which this other widget can use to update itself as necessary (for instance, resetting itself). This pattern is repeatable: any sufficiently large or complex area of an app (such as a single route) may hold its own mediator to encapsulate the details of its UI interactions and communicate with the Model layer with a small, well-defined API.

This flow is in contrast to our previous system, in which the component, rather than signaling its actions, instead directly set values on the UI state object, with other components and models listening for changes (and in turn performing their internal updates and updating the global state object again). The difference can be subtle in practice: in one system the component says, “I have been updated, now I will make the appropriate app state updates” (requires knowledge of the wider system, couples each component to the app-specific state object and thereby to all other components); while in the other, “I have been updated, others may take action if appropriate” (the component knows only its own internals, while app-specific UI logic is centralized in the mediator). They are both event-based, but one relies heavily on global state and handles extra complexity poorly, while the other keeps components decoupled and limited in scope.

On the basis of this standard pub/sub pattern, we derive a simple system for managing data flow through an app with many subviews and events. The main rules of the game are as follows:

  • user interactions with View components (menus, buttons, sliders, etc) trigger events
  • programmatic UI updates (e.g., setting the value of a dropdown in app code) never trigger events
  • Views listen for events only from their children, never from parent views

These basic principles specifically address the control flow problems noted in the previous section, by removing the potential for feedback loops and other unwanted/unexpected chain reactions. In a practical implementation, they lead to several further design characteristics:

  • View initialization and render are separate steps
  • no events are fired during the app’s initial bootstrap/render process
  • on a given UI screen, a single top-level mediator takes on all responsibilities of communication/event-dispatching between subviews

Effectively, what these seemingly abstract rules produce is a straightforward control flow mechanism in which any given UI action or model data update results in simple, predictable, finite execution – similar in its insistence on a unidirectional event flow to the Flux architecture (which others have gone so far as to characterize as a simple rebranding of “old-school procedural programming”).

Within a self-contained route or screen of a report app, the main View (the one instantiated by the app’s router) holds its own subviews and handles all the event-driven communication between them, serving as the mediator in the previous diagram and earning the name “Orchestrator.” In practice, the code of the Orchestrator view becomes the centralized location and source of truth for all inter-widget communication in its purview. Consequently, reading through the Orchestrator code offers a quick high-level overview of all subview interdependencies:

In contrast to a declarative approach which might abstract this setup phase into a data structure or specialized markup extensions (as is common for two-way data-binding), the imperative version above leaves no ambiguity as to when and how binding and updating takes place.

Looking within a subview such as DatePicker, user interaction may lead to internal updates, but ultimately triggers events to signal the action – in a manner which requires no knowledge of anyone else’s internals:

Conversely, when the Orchestrator receives news of an action or state change which should be reflected in the UI of DatePicker, it passes the data explicitly to the subview, which updates itself. In no case does a UI component trigger events as the result of messages from the Orchestrator, eliminating the problem of circular dependencies in the event chain.

An important characteristic of the system’s loose coupling is the fact that subviews (such as DatePicker and GraphSelector) have knowledge neither of each other, nor of the Orchestrator. They could be lifted out of the app and replaced or included as library components with minimal changes. Each subview is responsible only for its own child subviews (e.g., one of several dropdown menus within a complex widget), listening for messages from them as appropriate and ensuring that its overall UI remains in sync with data as given at initialization and updated later via calls to its update method.

Outcomes

With the largely imperative, plain-vanilla Backbone, flow of our restructured reports, there is explicit visibility into every step of the bootstrapping and rendering process. No one-off custom syntax to adhere to, no hidden helpers, no unexpected global state changes deep within a widget’s code. Data flows predictably between the Model and View layers, without the GOTO-like shortcuts offered by a global state object. UI components manage their own state and nothing more, without tight couplings to other app objects. The event bus which is now present, the Orchestrator view, serves as the single, easily-located center for all interactions between components of the overall system.

This is not a framework built on top of Backbone. These are patterns for how our organization uses Backbone, offering us some clearly advantageous separation of concerns, a solid Model layer, some conveniences in eventing and routing, and otherwise getting out of the way – which Backbone does eminently well, in contrast to many other solutions. Front-end development can work wonderfully without the extremes of tooling, libraries, and heavyweight declarative abstraction we sometimes want to saddle on it in search of a panacea. Keeping our control flow simple, and our components and SPAs small and decoupled, works all the magic we need. Addy Osmani got it exactly right: “at the end of the day, the key to building large applications is not to build large applications in the first place.”

If you're interested in what we work on, please apply - we're hiring: http://mixpanel.com/jobs/

11 thoughts on “Straightening our Backbone: A lesson in event-driven UI development

  1. Chris Harrington

    Very informative Backbone article. After having reviewed/considered other JS libraries, I have decided to pursue using Backbone myself. This article provides good architectural guidance. Do you have any links to code samples which follow this architecture?

    Reply
  2. Richard Klancer

    This sounds very similar to that “statechart” architectural pattern that was encouraged by SproutCore, and that was very successful in keeping my first single page app from turning into a big ball of mud.

    (SproutCore was a Cocoa-inspired JS library developed by Apple for MobileMe/iCloud. It was a little ahead of its time, but Ember started life as SproutCore 2.0 and retains some of its flavor).

    Specifically:

    – User events would cause views to dispatch a corresponding custom event “downward” to a central statechart object. That’s all.

    – The statechart would receive events, and depending on what state it was in, trigger a corresponding action or switch states (generally, the actions were just one or two lines that call controller methods that do any heavy lifting)

    – In response to being called by the statechart, the controller objects would perform any logic, modify model objects, and update the values to which views (which the controllers don’t know about, unless they are view controllers that set up and tear down views) are bound.

    (BTW, this architecture was slightly different than the diagram in the above link; I tended to use controllers “below” the statechart, as containers for app logic and corresponding model mutations, rather than simply as “view controllers” above the statechart that hold data to be shown by the views.)

    I found that this worked very nicely in practice. The statechart gave you a nice global picture of how your app worked, fits nicely with the inevitably stateful/modal nature of the user-app interaction, but in turn promoted a very clean separation of layers.

    In Ember, *some* of that functionality has been pulled into URL-based routers, but I don’t think the router is an adequate replacement for the mediator role of the statechart. I miss the statechart abstraction, I’d like more people to be aware of it, and I’d like to see more single page app architectures use it.

    Reply
  3. Justin Dennahower

    I’ve been down this path, if you stop thinking when you get to “events” you end up with a rats nest… an implicit rats nest. Think protocols not events.

    Reply
  4. Jordan Lev

    I too would be very interested in seeing a more fleshed-out code sample that utilizes these ideas. Thanks for the great article!

    Reply
  5. Pingback: Bookmarks for April 9th | Chris's Digital Detritus

  6. zachary kane

    I like the look of this but would appreciate a little clarification. At the orchestrator/mediator level, it alone is listening to the data layer, the model. When it receives notification that the model, in part or in whole, has changed it is allowed to call methods on its children/subviews?

    this.tableWidget.update();

    And this also happens when subviews are trying/needing to communicate? ala, this.datePicker.reset();

    So is the real organization/sanity coming from the simple fact lowest level children/subviews dispatch events with their values ‘upwards’, while mediators/(parents?) directly call methods ‘downwards’? That children/subviews never listen to the model they’re ultimately displaying?

    Sorry if this is very obvious, maybe it’s just the simplistic nature of the example.

    Reply
    1. Ted Dumitrescu Post author

      Yes, you’ve described the basic principle very well. The orchestrator calls methods directly on the main components it knows about (the top level of the UI hierarchy), and reacts to events from them in turn. We had some internal discussion around the question of whether to let subviews listen directly to events from the data model, rather than bouncing through the orchestrator. In principle, it’s perfectly fine for those views to listen to the model – that’s a very common Backbone pattern – but we opted to go through the orchestrator because it helped centralize the communication logic, viz. there’s a single place in the code where you can go to see all the wiring between top-level views and models. When it starts getting overwhelming, that’s probably a sign that the view hierarchy is too flat for the number of components you’re working with, and can be refactored with the same pattern.

      Reply
      1. Josh Reback

        I think I’m still a little confused…is it correct to say that:

        * The subviews only trigger events in respond to user actions, and respond to messages from the orchestrator (and/or, it seems, their parent views).
        * The orchestrator/mediator listens to events from the views, and responds by passing messages down to the subviews.
        * The orchestrator/mediator also updates the data model(s) in response to events which have bubbled up.

        My lingering confusions are as follows:

        1. I don’t see a clear distinction between the “Orchestrator”, “parent view” and “mediator” as they all seem to have some overlapping responsibilities. i.e, when is an event that has bubbled up handled by the Orchestrator vs. by the parent view? Is it just a function of how many other UI components depend on that event, e.g, more dependent UI components means more interdependencies, and therefore should be handled by the orchestrator?

        2. You mentioned in response to Zachary’s comment that in your approach, the views listen to the orchestrator instead of to the models directly. If the state of a UI component depends on data sent back from the server (which is reflected in the state of a model or collection), how do you pass that data to the view so it renders itself properly? I suppose I don’t really understand the mechanics of how you would have a view reflect server updates to a model without having views listen to models — in the code snippets above, what consequence does “this.model.refresh(itemAttrs)” have if no views are listening for changes on the model and the orchestrator doesn’t appear to be doing anything with the model aside from updating it? Furthermore, if you were using a backbone plugin such as marionette, which encourages binding to the model/collection directly by means of the modelEvents & collectionEvents hash, would you change your approach?

        Thanks in advance and apologies in advance if there’s something obvious I’m not understanding.

        Reply

Leave a Reply

Your email address will not be published. Required fields are marked *