React, AngularJS, and Om for Single Page Applications
Overview
In our first article on Single Page Applications we presented a REST JSON API running on Clojure with CouchDB. In this article we build on the previous article by introducing our client-side application.
React versus AngularJS
AngularJS is a popular Javascript MVC framework. It's one of our favorite approaches for building web and mobile applications.
Recently one of our clients needed a web application for monitoring data in real-time. Using AngularJS and WebSockets we built a web application that would update its client-side models as new information was available. AngularJS was perfect for this problem; however, we were concerned about the performance as the amount of data being monitored increased. AngularJS uses a digest loop to watch Javascript objects bound to the view so that it can make updates to the HTML DOM when the Javascript variables change, thereby keeping the UI synchronized. We knew the digest loop would slow and our testing showed that we began to see and feel the slowness when we reached around one hundred elements. Our view contains a large number of model bound fields with additional logic for row coloring, sorting, etc. A future version of AngularJS will address this performance issue by using Object.observe(), a new JavaScript API that browsers will use to notify subscribers of data changes, allowing the UI state to stay synchronized much more efficiently than current dirty checking methods allow. Until browsers support this new API an alternative approach is needed for large lists or grids of elements in AngularJS.
Lately there's been a lot of discussion about Facebook's React, a Javascript view engine that renders an HTML UI based on a Javascript model. React is the View in Javascript MVC and only addresses rendering, unlike AngularJS which includes routing, dependency injection, two-way data-binding, etc. React is getting a lot of attention because it offers some significant performance gains for view rendering in single page applications (SPA) specifically because it doesn't use a digest like loop to keep the UI and Javascript in sync. A second benefit for our application is that React only performs view rendering. It's possible to integrate with AngularJS so that React is used as an alternative to the AngularJS data-bound view module. This is a great hybrid approach that gives the application the best features of both frameworks.
ClojureScript and Om
In a recent article we wrote about how we are using Clojure with CouchDB to build a SPA. Here we continue our original article using ClojureScript for our client-side SPA programming. ClojureScript is a Clojure compiler that produces Javascript. Using ClojureScript feels really intuitive, but it would be intimidating to try to use ClojureScript to work with an expressive framework like React. Luckily the Om project has creted an interface for React. Using Om we can now build our client-side SPA. We started this article discussing slow performance of AngularJS rendering with large datasets. Now let's use ClojureScript and Om to see how our performance changes.
Setup
First we need a new API call from our original article. Our new REST API will return all of our records in our first article, instead of returning just one at a time. We'll add this to our handler.clj file.
(GET "/"
[]
(load-all))
Now to implement our load-all method and return all our CouchDB objects to our client. The snippet of clojure code below will use sequence to load all the CouchDB entities from our dataset. Each entity is a pair with the first item being the CouchDB key and the second being our object. We only want the object data so we use ```second with map to pluck the second element of the pair into a list and return to the client in the body of the get request.
(defn load-all
[]
{:body
(map #(second %)
(seq (db)))
})
Om Client Code
With our server changes above we're now ready to code our Om and ClojureScript client to consume our new REST API. We'll explain this code snippet after the break.
(ns spa-example.cs
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]
[cljs.core.async :refer [put! chan <!]]
[ajax.core :refer [GET json-response-format]]))
(def app-state
(atom
{:teams nil}))
(defn teams-view [app owner]
(reify
om/IRender
(render [this]
(apply dom/tbody nil
(map-indexed (fn [idx t]
(let [{:keys [team goals_for goals_against win loss draw points goal_diff games]} t]
(let [cols [(+ idx 1) team win loss draw goals_for goals_against goal_diff games points]]
(apply dom/tr nil
(for [r cols]
(dom/td nil r))))))
(sort-by :points #(compare %2 %1) (:teams app)))))))
(defn app-state-error [response]
(.error js/console (:message response)))
(defn app-state-handler [response]
(swap! app-state assoc :teams response)
(om/root teams-view app-state
{:target (. js/document (getElementById "content"))}))
(GET "/" {:handler app-state-handler :error-handler: app-state-error
:response-format (json-response-format {:keywords? true})})
Starting from the top, we declare our namespace in our client just like we would in a server-side clojure program. Here we are requiring Om and parts of ClojureScript to make our lives a little easier.
Next up we create our app-state. This atom stores our client-side application state, compared to angular this would be similar to an AngularJS scope.
After the app-state comes our function that defines a React component teams-view . Our teams-view function creates a new React component. Here our component creates a table body and the uses map to build the individual rows and columns of our table based on the data in the teams element of our app-state.
app-state-error and app-state-handler are the failure and success callback from the GET function that hits our server-side Clojure REST API. When GET is called it returns data to the app-state-handler. This data is swapped into our app-state, completely replacing whatever is there. The app-state-handler also calls om/root which renders our React component into the element named "content".
Om HTML File
Below is our Om HTML file. This file sets up the target for our React component when it is rendered.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"></meta>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>React Tutorial Rewritten in OM</title>
</head>
<body>
<!-- entry point for components -->
<table>
<thead>
<tr>
<th>Rank</th>
<th>Nation</th>
<th>W</th>
<th>L</th>
<th>D</th>
<th>GF</th>
<th>GA</th>
<th>GD</th>
<th>GP</th>
<th>Points</th>
</tr>
</thead>
<tbody id="content">
</tbody>
</table>
<!--<script type="text/javascript">
var CLOSURE_NO_DEPS = true;
</script> -->
<script src="//cdnjs.cloudflare.com/ajax/libs/react/0.11.1/react.js"></script>
<script src="js/client.js" type="text/javascript"></script>
</body>
</html>
AngularJS
Now that we've explained the React and Om approach, let's show this same solution using AngularJS. First we'll start with our AngularJS HTML file with our declarative markup.
<!DOCTYPE html>
<html ng-app="worldCupApp">
<head>
<title>AngularJS Performance Test</title>
</head>
<body ng-controller="ScoreController">
<table>
<thead>
<tr>
<th>Rank</th>
<th>Nation</th>
<th>W</th>
<th>L</th>
<th>D</th>
<th>GF</th>
<th>GA</th>
<th>GD</th>
<th>GP</th>
<th>Points</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(index,team) in teams | orderBy:points:true">
<td>{{index + 1}}</td>
<td>{{team.team}}</td>
<td>{{team.win}}</td>
<td>{{team.loss}}</td>
<td>{{team.draw}}</td>
<td>{{team.goals_for}}</td>
<td>{{team.goals_against}}</td>
<td>{{team.goal_diff}}</td>
<td>{{team.games}}</td>
<td>{{team.points}}</td>
</tr>
<tr>
<td colspan="9"> </td>
<td>{{total()}}</td>
</tr>
</tbody>
</table>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.js"></script>
<script type="text/javascript" src="client/app.js"></script>
</body>
</html>
Now for our AngularJS controller
(function() {
'use strict';
var worldcupApp = angular.module('worldCupApp', []);
worldcupApp.controller('ScoreController', function($scope, $q, $http, $filter ){
$scope.teams = [];
$scope.init = function(){
$http.get('http://localhost:3000/').then(function(result){
$scope.teams = result.data;
}, function(error){
console.log(error);
});
};
$scope.points = function(team){
return team.points;
};
$scope.sortedTeams = function(){
return $filter('orderby')($scope.teams, $scope.points, true);
};
$scope.total = function(){
var total = 0.0;
angular.forEach($scope.teams, function(t){
total += t.points;
});
return total;
};
$scope.init();
});
}());
Just like React, Angular makes an AJAX call to the Clojure REST API to load the dataset. It renders the results using a declarative approach in the HTML and only uses a few Javascript helper functions to do some aggregate calculations.
Conclusion
In this article we've shown just how simple it is to build a React view using Om with ClojureScript. We've also set the stage for our final article which discusses the performance difference between the two approaches, AngularJS vs Om. AngularJS is known for having a difficult learning curve, and Om is no different. In addition to being comfortable with React you also require knowledge of Clojure and ClojureScript, both large topics on their own. For teams considering an investment in Clojure, this is an easy trade and will help build your Clojure competency faster. In the next article we'll also expand on our Om example and bring in channels which will allow our UI to respond to changes in the data structures we are displaying.
Do you need an expert in web development? With a team of web development specialists covering a wide range of skill sets and backgrounds, The BHW Group is prepared to help your company make the transformations needed to remain competitive in today’s high-tech marketplace.