Pimp my Backbone.View (by replacing it with React)

Avatar von Paul Seiffert

I’ve been using Backbone.js in a couple of projects now and my feelings about it are quite diverse. On the one hand, I like how it provides you with guidelines on how to structure your frontend code. Although splitting model and view is a very basic idea in software development, it also is very powerful. Backbone.js is of great help by providing collections which aggregate model instances and by being able to sync these models with a server via RESTful APIs. On the other hand, it always (and I hope that it’s not just me) seems to be a pain in the b*** to figure out the best way to implement a proper view lifecycle and to keep track of all registered event handlers. If you aren’t careful when removing or even just re-rendering views, you can seriously mess up event handling and prevent proper garbage collection. If you are just a little sloppy, this leads to a slow frontend with an always increasing memory footprint.
I’m not saying that Backbone.js is bad in handling UI events, just that you have to care about too many things that are common to most web applications. This article describes an alternative to the Backbone.View component.

React pragmatically

A few days ago, Facebook and Instagram published a JavaScript framework of theirs: React.

React has some interesting concepts for JavaScript view objects that can be used to eliminate this one big problem I have with Backbone.js.
As in most MVC implementations (although React is probably just a VC implementation), a view is one portion of the screen that is managed by a controlling object. This object is responsible for deciding when to re-render the view and how to react to user input. With React, these view-controllers objects are called components. A component knows how to render its view and how to handle to the user’s interaction with it. The interesting thing is that React is figuring out by itself when to re-render a view and how to do this in the most efficient way. Let’s have a look at an example:

/** @jsx React.DOM */
var TodoListItem = React.createClass({
    render: function () {
        return 
  • {this.props.todo.text}
  • ; } }); var TodoList = React.createClass({ getInitialState: function () { // @TODO: Listen for changes on the underlying model and call // this.setState({ todos: updatedTodos }) // every time there is a change. return { todos: [{ text: 'Dishes!', dueDate: new Date() }] }; }, render: function () { var todos = this.state.todos.map(function (todo) { return ; }); return
      {todos}
    ; } }); React.renderComponent(, document.body);

    This short snippet of code is defining two view components: TodoListItem and TodoList. Every time a TodoList is created, it is equipped with one standard todo (I have to remind myself of this every day!). When being rendered, the list delegates the rendering of single todos to the TodoListItem component class. The resulting components are then placed into a <ul> element. You might think that this is some kind of pseudocode – it’s not! This is JavaScript with JSX which is being transformed to pure JavaScript before being executed. If you want to learn more about it, have a look at React’s documentation – it’s really simple to get started!

    So far, you haven’t seen more code than required to render a simple list. However, the simplicity of this short snippet gives you a good idea of the rest of the framework – it’s really not much more than that. The same functionality implemented with Backbone.js would look somewhat like this:

    var TodoListItem = Backbone.View.extend({
        tagName: 'li',
    
        render: function () {
            this.model.on('change', this.render, this);
            this.model.on('remove', this.remove, this);
    
            this.$el.html(this.model.get('text'));
        },
    
        remove: function () {
            this.model.off(null, null, this);
    
            Backbone.View.prototype.remove.apply(this, arguments);
        }
    });
    
    var TodoList = Backbone.View.extend({
        tagName: 'ul',
    
        listItems: null,
    
        initialize: function () {
            this.listItems = {};
        },
    
        render: function () {
            this.$el.empty();
    
            this.removeListItems();
    
            this.model.on('add', this.onTodoAdded, this);
            this.model.on('reset', this.render, this);
    
            this.model.each(function (todo) {
                var listItem = this.getListItemForTodo(todo);
    
                listItem.render();
    
                this.$el.append(listItem.$el);
            }, this);
        },
    
        remove: function () {
            this.removeListItems();
            this.model.off(null, null, this);
    
            Backbone.View.prototype.remove.apply(this, arguments);
        },
    
        removeListItems: function () {
            _.invoke(this.listItems, 'remove');
        },
    
        getListItemForTodo: function (todo) {
            if (_.isUndefined(this.listItems[todo.text])) {
                this.listItems[todo.text] = new TodoListItem({
                    model: todo
                });
            }
    
            return this.listItems[todo.text];
        },
    
        onAddTodo: function (todo, collection) {
            var index = collection.indexOf(todo),
                listItem = this.getListItemForTodo(todo);
    
            this.insertListItemAtIndex(listItem, index);
        },
    
        insertListItemAtIndex: function (listItem, index) {
            var $list = this.$el;
    
            if ($list.children().length < index) {
                $list.append(listItem.$el);
            } else {
                $list.children().eq(index).prepend(listItem.$el);
            }
        }
    });
    
    var list = new TodoList({
        model: new Backbone.Collection([
            {
                text: 'Dishes!',
                dueDate: new Date()
            }
        ])
    });
    list.render();
    
    list.$el.appendTo(document.body);
    

    This is at least the best approach to a proper handling the view lifecycle I have come across. As you can see: all the logic for keeping model and view in sync can be omitted with React.

    React asynchrously

    There still exists one problem when mixing Backbone.JS code and React code: Although the React sources itself can be loaded with an AMD loader (like require.js), the code written for the use with React contains these weird-looking snippets I explained to be JSX. Without an AMD loader this is not a problem: JSX source code is included in script tags with a specific type (<script type="text/jsx">...</script>). Scripts with this type are being detected by a the JXSTransformer which translates the JSX source code to pure JavaScript. If you know how require.js works, you can now figure out what the problem is.

    For every module you load with require.js, a new <script> tag is added to the of your HTML document. Sadly, it is not possible to specify the type of each of these <script> tags. To be able to load JXS source files with require.js, we have to use some other approach. This problem reminded me of another case in which a file that does not contain pure JavaScript code is loaded with require.js: templates. One common way to load templates with require.js is to use the text plugin. Therefore, I had a look at how require.js plugins work and whether it might be possible to load JSX code with a plugin, transform it in the client, and execute the resulting JavaScript code afterwards. What I came up with is now available on GitHub and can be used for exactly this use case.
    My plugin is certainly not production-ready, nor has it been tested excessively. However, you should give it a try and I would also appreciate some feedback and/or PRs!

    Now that we can load source files containing JSX code, we can finally combine Backbone.js and React.

    Reactive Backbone

    First, we need to set up a little Backbone.js application just like we would do without React. Whether you do this with Yeoman, just Bower, or manually doesn't really matter. You will end up with an HTML document similar to this one

    
    
        
            
            
        
        
            
        
    
    

    and some JavaScript file (in our case the app/main.js script) that configures the application:

    require.config({
        deps: ["main"],
        paths: {
            jquery: "../lib/jquery-1.9.1",
            lodash: "../lib/lodash.compat",
            backbone: "../lib/backbone",
            text: "../lib/text"
        },
        shim: {
            backbone: {
                deps: ["lodash", "jquery"],
                exports: "Backbone"
            }
        }
    });
    
    require(['app'], function(app) {
      app.run();
    });
    

    In the application's run method I would like to start Backbone.js' history and let a router control the application. In the router, I would then render view components. To be able to do this, the router needs to know about React and also contain some JSX code:

    define(['backbone', 'react', 'jsx!view/todoList'], function (Backbone, React, TodoList) {
      return Backbone.Router.extend({
        routes: {
          '*default': 'defaultAction'
        },
    
        defaultAction: function () {
          var todos = new Backbone.Collection([
            {
              text: 'Dishes!',
              dueDate: new Date()
            }
          ]);
    
          React.renderComponent(, document.body);
        }
      });
    });
    

    If we would try to load this file with the default require.js mechanism, we would probably get an Uncaught SyntaxError: Unexpected token <. Now we need to use the above mentioned require.js plugin:

    1. Get it from here
    2. Put it into your JavaScript directory
    3. Update your require.js configuration in a way that it can find the plugin and React
    4. Add the router to the main module's requirements

      require.config({
          deps: ["main"],
          paths: {
              jquery: "../lib/jquery-1.9.1",
              lodash: "../lib/lodash.compat",
              backbone: "../lib/backbone",
              jsx: "../lib/jsx",
              JSXTransformer: '../lib/JSXTransformer',
              react: '../lib/react'
          },
          shim: {
              backbone: {
                  deps: ["lodash", "jquery"],
                  exports: "Backbone"
              }
          }
      });
      
      
      require(['app', 'jsx!router'], function(app, Router) {
          app.router = new Router();
          app.run();
      });
      

    The important part here is that when specifying the dependencies of our main script, we reference the router with 'jsx!router'. This tells require.js to use the JSX plugin to load the file router.js.

    The resulting application can be found in the mentioned repository on GitHub in the demo directory.

    Code

    If you want to compare the three different approaches used to render this simple Todo list, have a look at the code:

    Hopefully, this article was able to highlight the benefits of combining React and Backbone.js and convince you to give it a try. If you have any questions or want to give me some feedback, feel free to add a comment or contact me via Twitter.

    In eigener Sache …

    Mit WebAssembly die Kosten für die Wartung Deiner Cloud-Anwendung sparen und die Wirtschaftlichkeit und Effizienz des Unternehmens steigern?

    Am 26. September 2024 um 11:30 Uhr bieten Dir unsere Experten einen tiefen Einblick in ein spannendes R&D-Projekt.

    Melde Dich jetzt kostenlos an!

    Avatar von Paul Seiffert

    Kommentare

    3 Antworten zu „Pimp my Backbone.View (by replacing it with React)“

    1. Lesenswert: Pimp my Backbone.View (by replacing it with React) http://t.co/7fjzkHWScn

    2. this is an excellent article, thanks!! perhaps you can add some information about how JSX and r.js interact. this info is still relevant, thanks so much!

    Schreibe einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


    Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.