Clay Allsopp

Moving From Backbone To React

Jun 22, 2013

We've been building a fairly big, interaction-heavy JavaScript app for Propeller (you can check out a slice of it on our homepage). We started with Backbone as the base for most of our code, but we started to feel some pain at the view layer as our app grew larger.

Then, just as we started to looking for new solutions, React appeared.

What is React? In effect, React is a replacement for Backbone.View. It's not a huge application-level framework like Angular or Ember, and it can peacefully coexist with existing components like Backbone.Model.

Why did we replace Backbone.View? The biggest headache with vanilla Backbone is once you start nesting Backbone.View objects, you're basically on your own with regards to managing the lifecycle of the view hierarchy.

See, for every subview, you have to remember to a) tear it down b) respect its internal state when its parent is #render'd. You'll often encounter code like this, if not more complex:

ProfileView = Backbone.View.extend({
    render: function() {
        var template = _.template($("#profile-view-template").html());
        $(this.el).html(template);

        // Add a subview
        if (!this._avatarView) {
            this._avatarView = new AvatarView({model: this.model});
        }

        $(".avatar-container", this.el).html(this._avatarView.render().el);

        // ... Add a bunch of other subviews

        return this;
    },
    remove: function() {
        // Remove a subview
        this._avatarView.remove();

        // ... Remove a bunch of other subviews

        return Backbone.View.prototype.remove.apply(this, arguments);
    }
});

Subviews involve a lot of easy-to-forget boilerplate that Backbone (by design) doesn't automate. Libraries like Backbone.Marionette offer more abstractions to make view nesting easier, but they're all limited by the fact that Backbone delegates how and when view-document attachment occurs to the application code.

React, on the other hand, manages the DOM and only exposes real nodes at select points in its API. The "elements" you code in React are actually objects which wrap DOM nodes, not the actual instances which get inserted into the DOM. Internally, React converts those abstractions into real DOMElements and fills the document accordingly.

To make that a little clearer, our ProfileView example turns into something like this using React (without JSX, which we'll get to later):

ProfileView = React.createClass({
    render: function() {
        return React.DOM.div({},
        [
            React.DOM.div({className: "avatar-container"},
            [
                AvatarView({model: this.getModel()})
            ])
        ]);
    }
});

// Outside of your React code:
var view = ProfileView({model: User.profile()});
React.renderComponent(view, $("#profile-container")[0]);

Now our AvatarView's clean-up code will be called automatically, and successive calls to ProfileView#render will be intelligently diff'd to update the DOM when necesssary. In other words, less memory leaks and faster interfaces.

JSX

React also ships with JSX, a quasi-preprocessor-language. JSX is a custom XML-ish syntax which compiles into vanilla JavaScript. Our ProfileView example above would look more like this using JSX:

/** @jsx React.DOM */
ProfileView = React.createClass({
    render: function() {
        return (
            <div>
                <div className="avatar-container">
                    <AvatarView model={this.getModel()} />
                </div>
            </div>
        );
    }
});

JSX makes React code easier to read and write when dealing with a complex hierarchy, but it's not required. And since JSX elements get converted to normal JavaScript, we can use them anywhere we would use plain-old objects:

/** @jsx React.DOM */
UserListView = React.createClass({
    render: function() {
        var listElements = _.map(this.props.users, function(user) {
            return (<UserView user={user} />);
        });
        return (
            <ul>
                { listElements }
            </ul>
        );
    }
});

Migrating

Once we figured out that React is what we were looking for, it was time to move our classes over. The pattern for this is:

  1. Pick a Backbone.View subclass to migrate.
  2. Use our react.backbone helpers as a starting point.
  3. React-ify the code: change your #render code to use JSX, move code from #initialize to #componentDidMount, and move code from #remove to #componentWillUnmount.
  4. Convert the parent view to use React.renderComponent instead of something like $.append.

At Propeller, our JSX workflow is:

  1. Write JSX views in .jsx files
  2. Add a daemon to watch .jsx file changes
  3. When a change occurs, trigger the jsx tool to compile our files into .js variants:
// Monitors `.jsx` files in `js/views` and outputs the result to `js/views/_jsx`
jsx ./js/views ./js/views/_jsx -x jsx

Those compiled .js files in views/jsx are what end up getting served in our HTML files.

There is also an in-browser JSX compiler which will do this process at runtime while your app loads, but I wouldn't recommend using it even for rapid prototyping. It makes debugging crashes in your JSX snippets challenging, as stack-traces often get mangled with the compiler's own (sizable) implementation.

Gotchas

React is used in production at Instagram (the entire page) and Facebook (comments), so it's far from unstable, but there are still some quirks:

  • Say goodbye to your CSS #id selectors. React currently generates DOM element IDs on all React-generated elements, so your CSS and JQuery selectors will need to be changed to work with exclusively .class's. Check out Instagram's markup to see what I'm talking about.
  • JSX looks like normal HTML, but it isn't. If you want to specify a custom DOM property (like ng-something), you'll need to do that in #componentDidUpdate with the actual DOMElement instance. React elements will accept DOM properties prefixed with data- or aria-, but if you have your own custom scheme you'll have to do it elsewhere.
  • As of React 0.3.2, you need to include that /** @jsx React.DOM */ comment in all your JSX files for React's tools to work correctly, which took awhile to figure out the first time. There's an open issue about removing it on Github.

Worth It?

We moved about 20 different Backbone view classes to React over the past few weeks, including the live-preview pane that you see in our little iOS demo. Most importantly, it's allowed us to put energy into making each component work great on its own, instead of spending extra cycles to ensure they function in unison. For that reason, we think React is a more scalable way to build view-intensive apps than Backbone alone, and it doesn't require you to drop-everything-and-refactor like a move to Ember or Angular would demand.