Tuesday, August 5, 2014

Isomorphic apps with Backbone.js and React

Ever since AJAX came into the scene more than 10 years ago, developers have been looking for a way to build fast, efficient, and powerful web applications with as little maintenance nightmares as possible. Many developers now prefer to create single page applications (SPAs), delegating the entire UI rendering to the client. However, SPAs come with a set of problems:

  • SEO
    Since your server basically returns a blank page, crawlers have no way to index your content. There are some ways to fix this problem, but it requires additional code and maintenance issues.
  • Performance
    Your application now has to wait for all of the Javascript to load before rendering something on the screen. This initial time before content is rendered (called time to first tweet by Twitter) has a huge impact on the perceived-speed of your application.
  • Accessibility
    Even though it is a very low percentage, there are people out there who don't have Javascript enabled on their browser. For these people, a SPA is as good as nothing.
  • Stateful URLs
    This is an issue that can be solved if taken enough consideration, but that many SPAs still suffer from. In an ideal world I would be able to copy the current URL and send it to a friend, and expect that friend to see the same content as me. However, if your app handles state changes without modifying the URL, then the whole URL concept breaks down.
  • Complexity
    On the old days, the request-response cycle was always the same:
    1. A client requests a route.
    2. The route is matched to a controller.
    3. The controller fetches one or more models from the database.
    4. The view is rendered with those models.
    MVC was simple and clean.

    Now days we can't even talk about MVC client-side. There is such a mess of patterns, concepts, and terminology that we have just opted for naming MV* anything that somehow separates data from presentation. This whole mess of concepts and abstractions means more things to keep in mind while developing, and more things to track down while debugging.

Isomorphic Apps


There is a new trend picking up strength in the web development world: Isomorphic Apps. The core idea is pretty simple: create applications that can be rendered both client-side and server-side with little or no code changes.

I decided to build my own Isomorphic App, trying to keep things as simple as possible. You can see the app running here, and you can find the source code here. What's interesting about this app is that it always renders the full HTML from the server. After the javascript loads, it takes over the HTML and converts it to a SPA. This means the app is rendered and fully working even before the JS finishes loading (you can even use the app with javascript disabled).

In the rest of this post I will explain how to build it.

Routes


I decided to write my routes using the RFC 6570 URI Templates format, so they would be parseable by the uri-templates library.

My app only has two routes:
  1. Index: displays either the latest or the most popular posts. You decide which list to show by modifying the "display" query param.
  2. Show Post: displays a single post.

My routes.js file looks like this:
/*-- routes.js --*/

define(function(){
  return {
    '/posts/{id}': 'show_post',
    '/{?display}': 'index'
  }
});
Each route is made from a uri-template and a controller id. The controller id will be used by require.js to load the controller.


Controllers


On my application, a controller has 3 objectives:
  1. Determine which view to render.
  2. Determine which params to pass to the view.
  3. Determine which data to fetch from the datastore.
This is how the index controller looks like:
/*-- controllers/index.js --*/

define([
  'views/index',
  'collections/posts/latest',
  'collections/posts/most_popular'
], function(IndexView, LatestPosts, MostPopularPosts){

  return {

    // 1. which view to render
    view: IndexView,

    // 2. which params to pass to the view
    params: function(urlObj, getURL){

      // urlObj contains the parsed route
      // if no display param was sent, we'll display the latest posts.
      var display = urlObj.display || 'latest';

      // the collection to use depends on the display param
      var collection = {
        latest: LatestPosts,
        popular: MostPopularPosts
      }[display];

      // these are the params sent to the view
      return {
        getURL: getURL,
        display: display,
        posts: new collection()
      };

    },

    // 3. which data to fetch from the datastore
    mustFetch: function(params){
      return ['posts'];
    }

  };

});
As you can see, the controller doesn't actually fetch the data or render the view. It only specifies what should be done. You might have noticed the params function takes 2 arguments:
  1. urlObj: an object with the result from parsing the route against the uri template.
  2. getURL: a function that lets views render urls easily (we'll see it in action later)

Models


I'm using regular Backbone.js models and collections. I also use the Supermodel.js plugin on some models, but that is not required for the isomorphic app to work.

Views


I decided to use React components as views. React works especially well with our lifecycle because we can re-render the whole app on every route change without incurring in performance penalties. To read more about React's virtual dom and why it's so efficient, check out Why did we built React?

This is how the index view looks like:
/*-- views/index.js --*/

/** @jsx React.DOM */

define([
  'react',
  'underscore',
  'mixins/backbone_binding'
], function(React, _, BackboneBinding){

  var TABS = {
    latest: { text: 'Latest', url: '/?display=latest' },
    popular: { text: 'Most Popular', url: '/?display=popular' }
  };

  return React.createClass({
    displayName: 'Index',

    render: function(){
      return (
        <div>
          <div>
            { this.renderTab('latest') }
            { this.renderTab('popular') }
          </div>
          { this.renderPosts() }
        </div>
      );
    },

    renderTab: function(name){
      var attrs = TABS[name];

      var classes = "index__tab";
      if(this.props.display === name){
        classes += " index__tab--active";
      }

      return (
        <a href={ attrs.url } onClick={_.partial(this.onClick, attrs.url)}
           className={ classes }>{attrs.text}</a>
      );
    },

    renderPosts: function(){
      return this.props.posts.map(function(post){
        var url = this.getPostUrl(post);
        return (
          <div className="index__post" key={post.id}>
            <a onClick={_.partial(this.onClick, url)} href={url}>
              {post.get('name')}
            </a>
          </div>
        );
      }, this);
    },

    getPostUrl: function(post){
      return this.props.getURL('show_post', { urlParams: {id: post.id} });
    },

    onClick: function(url, evt){
      evt.preventDefault();
      this.props.getURL(url, { pushState: true });
    },

    mixins: [
      new BackboneBinding({ model: 'posts', events: {
        'sync': 'onPostsChange'
      }})
    ],

    onPostsChange: function(){
      this.forceUpdate();
    }

  });

});
This is a pretty standard React component, except for two things:
  • Line 59: I'm using the getURL function that was passed down by the controller. It takes two arguments: the route id and the params that must be substituted in the url.
  • Lines 67-75: I'm using a Backbone Mixin so the view is re-rendered any time the posts collection triggers a 'sync' event.

The Orchestrator


As you can see, the MVC pieces of the app are pretty simple. All that's missing is some piece of code to tie them all together. I decided to write two orchestrators, a client-side orchestrator and server-side orchestrator. They both do the same actions:
  • Parse the current route against the uri-templates.
  • Fetch the corresponding router.
  • Find out which view to render, along with the params that must be sent.
  • Fetch data from the datastore.
  • Render the view with the correct params and data.

Even though they share most of the code, there are some things that are done differently on each side, for example:
  • On the client-side we find the current url from window.location. On the server-side we get the current url from the request.
  • On the client-side we render the view and then fetch data asynchronously, re-rendering when each request is completed. On the server-side we fetch all data before rendering.

Conclusion


After this small experiment with Isomorphic apps, I'm starting to see a lot of benefits from this approach. Even though it was pretty easy to implement using current libraries, I expect to see new tools being created in the near future to make writing isomorphic apps even easier to accomplish.

Like I mentioned before, you can take a look at the source code here. Please let me know in the comments if you find anything that can be improved.
Post a Comment