eHarmony Engineering logo

Using React as Backbone’s view layer

Kaanon MacFarlane

January 15, 2016


Here at eHarmony, we’re always keeping an eye out for new technologies. This serves a dual purpose: to see if there are opportunities to accelerate our product development velocity and to keep our engineering staff challenged and engaged. React has been gaining traction as a front-end technology and we wanted to try it out. At the same time, we have a pre-existing front-end library that we love: Backbone.
Kaanon MacFarlane

In this post, I’ll explore augmenting our Backbone views with React Components, no plugins required. This allows us to use React’s advantages without needing to revisit the backend communication provided by Backbone’s Collections and Models. To that end, I’ve moved all interactions typically handled within a Backbone.View to a simple React Component. In the code examples that follow, I’ll be using Babel and Webpack to call CommonJS style require statements.

As a demo application, I’ll be interacting with an API that returns a JSON response containing a list of information on some of our success couples, who met on eHarmony. I’ll show a paginated list of the couples with filtering by category.

The Code

index.html

<!doctype html>
<html>
<body>
    <div id='data-viewer'></div>
    <script src='bower_components/jquery/dist/jquery.js'></script>
    <script src='bower_components/underscore/underscore-min.js'></script>
    <script src='bower_components/backbone/backbone-min.js'></script>
    <script src='bower_components/react/react.js'></script>
    <script src='bower_components/react/react-dom.min.js'></script>
    <script src='js/build/data-viewer.js'></script>
</body>
</html>

First, include the necessary libraries. Here, I’m utilizing Bower, a package manager for the web, that allows for including client-side javascript much in the same way npm is used for Node.js. I’m using the #data-viewer element for the Backbone View to populate.

app.js

var DataCollection = require('./collections/DataCollection'),
    DataView = require('./views/DataView');

var myCollection = new DataCollection();
var myView = new DataView({collection:myCollection});

// Fetch The collection and then render it
myCollection.fetch().then(function(){
    myView.render();
});

Our app.js file is a simple creation of a collection and view. Backbone’s collections return a Promise; when resolved, we know that it’s time to render the view.

collections/DataCollection.js

var DataCollection = Backbone.Collection.extend({
    url: '/api',
    parse: function(data){
        var rows = data.rows.map(function(obj){
            // get rid of data we don't need
            obj = _.omit(obj,['category_id','status']);
            // Add a proper image path
            obj.image = 'http://static.eharmony.com/assets/success/couples/thumbs/' + obj.image;
            return obj;
        })
        return rows;
    },
    loadMore: function(){
        this.fetch();
    }
});
module.exports = DataCollection;

This is a very simple collection that retrieves data from an API. Because we are still using Backbone, we can utilize the built-in functionality that parses our API response.

views/DataView.js

var DataViewer = require('../components/data-viewer.jsx');
var DataView = Backbone.View.extend({
    el: '#data-viewer',
    initialize: function(){
        // Only allow the following filter options
        this.filterBy = 'category';
        this.contentKey = 'excerpt';
        this.titleKey = 'heading';
    },
    render: function(){
        this.reactView = React.createElement(DataViewer, {collection: this.collection, view: this });
        ReactDOM.unmountComponentAtNode(this.el);
        ReactDOM.render(this.reactView, this.el);
    }
});
module.exports = DataView;

Here we have a normal Backbone.View file. You’ll notice our render function creates a React Element and mounts the component to this.el, the HTML element the content will live in.
I pass the collection that holds our API data as well as the view as props to my DataViewer React component. This allows these objects to be accessed within React.

From this point, almost all interactions are handled within the React Components.

components/data-viewer.jsx

var DataViewer = React.createClass({
    /**
     * The initial state of this page
     * @method getInitialState
     */
    getInitialState: function () {
        return {
            filter: 'all',
            filterBy: this.props.view.filterBy,
            page: 1,
            perPage: 5
        };
    }
});
module.exports = DataViewer;

Here you can see how we can access properties of the Backbone View I passed in as a property. The remarkable part of React is that it’s “just” javascript, and you can access and alter properties and methods in the same fashion you would in any other Javascript code. In this example, I reference the filterBy property that was added in the initialize method of the DataView.js file.

components/data-viewer.jsx

var Pagination = require('./pagination.jsx');
var List = require('./list.jsx');
var Filters = require('./filters.jsx');

var DataViewer = React.createClass({
    /**
     * Get the filtered items for use when rendering
     * @method getFilteredQuestions
     * @return {array}
     */
    getFilteredModels: function(){
        var filterBy = this.state.filterBy,
            currentFilter = this.state.filter,
            filteredModels = [];

        if(this.state.filter === 'all'){
            filteredModels = this.props.collection.models;
        } else {
            filteredModels = this.props.collection.filter(function(model){
                return model.get(filterBy) === currentFilter;
            });
        }
        return filteredModels;
    },
    /**
     * Render the collection
     * @method render
     */
    render: function () {
        var startIdx = (this.state.page - 1) * this.state.perPage,
            endIdx = this.state.page * this.state.perPage,
            rows = this.getFilteredModels(),
            displayItems  = rows.slice(startIdx, endIdx),
            numPages = Math.ceil(rows.length / this.state.perPage);

        return (
        <section>
            <h2>eHarmony Success Couples</h2>

            <Filters collection={this.props.collection} filterOptions={this.props.view.filterOptions} filter={this.state.filter} filterBy={this.state.filterBy} updateFilter={this.onUpdateFilter} />

            <List collection={displayItems} titleKey={this.props.view.titleKey} contentKey={this.props.view.contentKey} />

            <Pagination changePage={this.changePage} page={this.state.page} numPages={numPages} />

            <a onClick={this.loadMore}>Load More</a>
        </section>
        );
    }
});
module.exports = DataViewer;

This is our first interaction with JSX, the templating language most often used with React. Initially I had many concerns about mixing the template into the Javascript code, however after using it, I’ve concluded that using JSX is no different from using Handlebars (our normal choice) – or Jade or Backbone templates, etc. Either way, I’m making it easier to output HTML in the desired format. The only difference is that it now lives in the same file, and can be accessed more easily.

You’ll notice that the items returned from getFilteredModels are the models that are part of this collection. Once again, because this is still a normal Backbone collection, I can utilize the Underscore’s filter utility method.

Lastly, you’ll see the various additional components that we are using.

components/list.jsx

/**
 * Data View
 * Component for the list of items
 */
var List = React.createClass({
    render: function () {
        var titleKey = this.props.titleKey,
            contentKey = this.props.contentKey;

        // Loop through the collection to get each display item
        var list = this.props.collection.map(function(model){
            var content = '',
                title = model.get(titleKey),
                content = model.get(contentKey),
                tableRows = [],
                value;

            return (
            <article key={model.attributes.id} className={model.attributes.type}>
                <h4>{title}</h4>
                <img src={model.attributes.image}/>
                <blockquote>
                    {content}
                </blockquote>
            </article>
            );
        });
        return (
            <div className='list'>
            {list}
            </div>
        );
    }
});
module.exports = List;

Shown here is the List view that loops through the models within the collection and returns the proper template for the “parent” component to render. We see that because this is a normal Backbone.model, the model.get method can be called in the normal fashion.

collections/DataCollection.js

var DataCollection = Backbone.Collection.extend({
    url: '/api',
    initialize: function(){
        this.page = 1;
    },
    loadMore: function(){
        this.page++;

        // Fetch the next page and add it to the list
        this.fetch({
            data: { page: this.page },
            remove:false
        });
    }
});

Here we’ve altered the Collection to allow for fetching additional items from our API.

components/data-viewer.jsx

module.exports = React.createClass({
    // This happens before the initial render of the component
    componentWillMount: function(){
        var that = this;
        // NOTE: When the collection updates, force a re-render
        this.props.collection.on('update', function(){
            that.forceUpdate();
        });
    },
    loadMore: function(){
        // NOTE: direct communication with the backbone collection
        this.props.collection.loadMore();

        var parentElement = ReactDOM.findDOMNode(this).parentNode,
            topOffset = $(parentElement).offset().top - 60;
        $('body,html').animate({scrollTop:topOffset + 'px'});
    },

We want the React component to know about the any changes made in the collection, and the result is surprisingly easy.

Just listen for changes

this.props.collection.on('update', function(){
    that.forceUpdate();
});

Here we utilize the existing Backbone.Events to listen for a change in the collection that we passed in. Using this technique, any alterations on the collection instance will trigger an update of the React Component. Because of React’s one-way data flow and shadow DOM, the parts of the component that need to be updated get done automatically. That’s right, AUTOMATICALLY! If I was doing this within a normal Backbone View, I’d have to re-render the entire view. React uses a shadow DOM to make alterations before applying them to the actual DOM in the browser.

Synopsis

In short, React is easy to integrate into an existing Backbone system because it’s just javascript, and it’s just the UI. By passing the models and collection to components as properties, any Backbone functionality can be used quite easily. And because both Backbone and React can be made aware of the changes to the underlying data, re-rendering is automatic.

Things I Like

  • Ability to pass in handlers naturally obviates the need for a data-action or ng-controller, or some other way to signify the binding.
  • Backbone can do all the model manipulation or api communication.
  • One-way data flow and re-rendering means I don’t need to explicitly specify which elements require an update.
  • Components can be re-usable with little changing. In the course of developing this post, I changed the api I wanted to use in the demo. I didn’t need to change pagination or filters in any way.
  • Speed of development. I finished this faster than I would have with Handlebars.
  • After finishing the look of the filters, getting them to update the rest of the components was as easy as setting the state.
  • Template Encapsulation.

Things I don’t like

  • if/then statements in JSX are terrible. I greatly prefer the {{#if condition}} helper in Handlebars.
  • React components need to use className instead of class.
  • The names of the events in React’s lifecycle are peculiar. componentWillMount makes less sense than, say, beforeMount

Finished Demo

View the demo
View the completed source files on Github