Using React components as Backbone Views

At Venmo, we've begun rewriting and redesigning our front-end into clean, idiomatic Backbone code. Backbone is an unopinionated framework that provides structure to your code. However, its view layer is purposefully lacking, providing only a few basic lifecycle hooks. Unlike Ember components or Angular directives, it does not hook your data up to your views for you, and does little to enforce separation between your layers.

While we've gotten surprisingly far using only vanilla Backbone views, we've begun exploring more advanced options for architecting our UI. My goal was to find something that could interface with Backbone views that gave me the data binding and scoping that I was used to from views in larger frameworks. Thankfully, my friend Scott pointed me at React, and after a few hours of wrestling with it, I came away very impressed.

Overview

React is a relatively new library by Facebook for creating isolated components. In many ways, it's similar to Angular directives or Polymer web components. A React component is essentially a custom DOM element with its own scope. It cannot directly interact with other portions of your application's state, whether in JavaScript or in the DOM.

The most unique - and most controversial - part of React is its use of JSX, which transforms HTML written inline with your code into parseable JavaScript. Here's an example React component that uses JSX to render a link:

/* @jsx React.DOM /
var component = React.createClass({
  render: function() {
    return <a href="http://venmo.com">Venmo</a>
  }
});

This gets transformed into:

/* @jsx React.DOM /
var component = React.createClass({
  render: function() {
    return React.DOM.a( {href:"http://venmo.com"}, "Venmo")
  }
});

Integrating JSX with your workflow is easy, thanks to the plethora of tools for compilation:

JSX is optional - you can write your templates using the React.DOM DSL if you'd like, though I'd only recommend this for simple templates.

Instead of teaching the basics of React in this post, I'll pass that responsibility to the excellent React tutorial. It's a bit long, but give it a skim before continuing!

Rendering a component from a Backbone view

Let's create a very basic component: a link that fires an event when clicked. We'd like to render this component as part of a Backbone view, rather than use it on its own. The component is easy enough to create:

var MyWidget = React.createClass({
  handleClick: function() {
    alert('Hello!');
  },
  render: function() {
    return (
      <a href="#" onClick={this.handleClick}>Do something!</a>
    );
  }
});

This is almost the same as the above example, except for the addition of a handleClick handler for the link's click event. Now, all we need to do is add a Backbone View to render this in:

var MyView = Backbone.View.extend({
  el: 'body',
  template: '<div class="widget-container"></div>',
  render: function() {
    this.$el.html(this.template);
    React.renderComponent(new MyWidget(), this.$('.widget-container').get(0));
    return this;
  }
});

new MyView().render();

Here's our completed new component:

Component -> Backbone communication

Of course, to really take advantage of React components, we'll need to be communicate changes from React to Backbone. For a very arbitrary example, let's say that we want to display a bit of text if the link in our component has been clicked. Not only that, but we want the bit of text to be outside the component's DOM element.

While the React docs are very good at explaining how to have different components communicate, it's not super obvious how a child component could interact with a parent Backbone view. However, there's a single, easy way to communicate: we can bind an event handler through the component's properties.

Here's an example of this pattern, in JSX:

function anEventHandler() { ... }
React.RenderComponent(<MyComponent customHandler={anEventHandler} />,
                      this.$('body').get(0));

We can do the same from our Backbone view, but instead of using JSX, we'll use the class itself:

var MyView = Backbone.View.extend({
  el: 'body',
  template: '<div class="widget-container"></div>' +
            '<div class="outside-container"></div>',
  render: function() {
    this.$el.html(this.template);

<span class="nx">React</span><span class="p">.</span><span class="nx">renderComponent</span><span class="p">(</span><span class="k">new</span> <span class="nx">MyWidget</span><span class="p">({</span>
  <span class="na">handleClick</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">clickHandler</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
<span class="p">}),</span> <span class="k">this</span><span class="p">.</span><span class="nx">$</span><span class="p">(</span><span class="s1">'.widget-container'</span><span class="p">).</span><span class="nx">get</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>

<span class="k">return</span> <span class="k">this</span><span class="p">;</span>

}, clickHandler: function() { this.$(".outside-container").html("The link was clicked!"); } });

And, inside the component, let's bind onClick to the new handler:

var MyWidget = React.createClass({
  render: function() {
    return (
      <a href="#" onClick={this.props.handleClick}>Do something!</a>
    );
  }
});

Here's our full, updated example:

Again, this is a very contrived example, but it shouldn't be hard to envision a more practical use case.

For example, at Venmo, we've remade our "Connect with Facebook" button with React. The actual Facebook API calls happen within the component, but the views in which the component are used bind different handlers to the component's events. These events (such as "authenticated with Facebook" or "logged out of Facebook") are essentially the component's "public API." React can also pass arguments with events, so that when a user connects with Facebook, the Backbone view gets the user's Facebook ID and attaches it to the user model.

Backbone -> Component communication

Now that we know how to get communication from a component, the next step is to be able to update the component's state from a Backbone model - as a concrete example, we'll make a view that reacts to a changed field on a model. This requires a bit of boilerplate (though no more than your usual manually-connected Backbone view), but is quite easy in practice.

First, we create a tiny model class:

var ExampleModel = Backbone.Model.extend({
  defaults: {
    name: 'Backbone.View'
  }
});

And a simple React component that displays the name field:

var DisplayView = React.createClass({
  render: function() {
    return (
      <p>
        {this.props.model.get('name')}
      </p>
    );
  }
});

However, this component on its own can't react to the model's field being changed. Instead, we can add a listener for the model's change event that tells the component to re-render:

var DisplayView = React.createClass({
  componentDidMount: function() {
    this.props.model.on('change', function() {
      this.forceUpdate();
    }.bind(this));
  },

render: function() { // ... } });

Then, we add another component that will actually change the name field:

var ToggleView = React.createClass({
  handleClick: function() {
    this.props.model.set('name', 'React');
  },
  render: function() {
    return (
      <button onClick={this.handleClick}>
        model.set('name', 'React');
      </button>
    );
  }
});

Finally, we create a model and render both components using JSX:

var model = new ExampleModel();

React.renderComponent(( <div> <DisplayView model={model} /> <ToggleView model={model} /> </div> ), document.body);

And the completed example:

Conclusion

Backbone + React is a fantastic pairing. There's several other blog posts that discuss it:

React also has an example Backbone+React TodoMVC app that's worth checking out.

While React is still immature, it's a very exciting library that seems ready for production use. It makes it very easy to share and reuse components, and I'm excited that it integrates so well with Backbone, making up for the default of view logic.