Moving From Backbone To React
Jun 22, 2013We'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:
- Pick a
Backbone.View
subclass to migrate. - Use our react.backbone helpers as a starting point.
- React-ify the code: change your
#render
code to use JSX, move code from#initialize
to#componentDidMount
, and move code from#remove
to#componentWillUnmount
. - Convert the parent view to use
React.renderComponent
instead of something like$.append
.
At Propeller, our JSX workflow is:
- Write JSX views in
.jsx
files - Add a daemon to watch
.jsx
file changes - 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 withdata-
oraria-
, 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.