So far, we have taken a journey through managing complexity by efficiently handling and modeling asynchronous workflows in terms of streams of data. In particular, Chapter 4, Introduction to core.async, and Chapter 5, Creating Your Own CES Framework with core.async, explored what's involved in libraries that provide primitives and combinators for Compositional Event Systems (CES). We also built a simple ClojureScript application that made use of our framework.
One thing you might have noticed is that none of the examples so far have dealt with what happens to the data once we are ready to present it to our users. It's still an open question that we, as application developers, need to answer.
In this chapter, we will look at one way to handle Reactive User Interfaces in web applications using React[1], a modern JavaScript framework developed by Facebook, as well as the following topics:
With the rise of single-page web applications, it became a must to be able to manage the growth and complexity of a JavaScript code base. The same applies to ClojureScript.
In an effort to manage this complexity, a plethora of JavaScript MVC frameworks have emerged, such as AngularJS, Backbone.js, Ember.js, and KnockoutJS, to name a few.
They are very different, but share a few common features:
- Giving single-page applications more structure by providing models, views, controllers, templates, and so on
- Providing client-side routing
- Employing two-way data binding
In this chapter, we'll be focusing on the last goal.
Two-way data binding is absolutely crucial if we are to develop even a moderately complex single-page web application. Here's how it works.
Suppose we're developing a phone book application. More than likely, we will have a model—or entity, map, or what have you—that represents a contact. The contact model might have attributes such as name, phone number, and email address.
Of course, this application would not be all that useful if users couldn't update contact information, so we will need a form that displays the current details for contact and lets you update the contact's information.
The contact model might have been loaded via an AJAX request, and they might have used explicit DOM manipulation code to display the form. This would look something like the following pseudocode:
function editContact(contactId) { contactService.get(contactId, function(data) { contactForm.setName(data.name); contactForm.setPhone(data.phone); contactForm.setEmail(data.email); }) } But what happens when the user updates someone's information? We need to store it somehow. Upon clicking on save, a function such as the following would do the trick, assuming you're using jQuery:
$("save-button").click(function(){ contactService.update(contactForm.serialize(), function(){ flashMessage.set("Contact Updated.") }) This seemingly harmless code poses a big problem. The contact model for this particular person is now out of date. If we were still developing web applications the old way, where we reload the page at every update, this wouldn't be a problem. However, the whole point of single-page web applications is to be responsive, so it keeps a lot of state on the client, and it is important to keep our models synced with our views.
This is where two-way data binding comes in. An example from AngularJS would look like the following:
// JS // in the Controller $scope.contact = { name: 'Leonardo Borges', phone '+61 xxx xxx xxx', email: '[email protected]' } <!-- HTML --> <!-- in the View --> <form> <input type="text" name="contactName" ng-model="contact.name"/> <input type="text" name="contactPhone" ng-model="contact.phone"/> <input type="text" name="contactEmail" ng-model="contact.email"/> </form> Angular isn't the target of this chapter, so I won't dig into the details. All we need to know from this example is that $scope is how we tell Angular to make our contact model available to our views. In the view, the custom attribute ng-model tells Angular to look up that property in the scope. This establishes a two-way relationship in such a way that when your model data changes in the scope, Angular refreshes the UI. Similarly, if the user edits the form, Angular updates the model, keeping everything in sync.
There are, however, two main problems with this approach:
- It can be slow. The way Angular and friends implement two-way data binding is, roughly speaking, by attaching event handlers and watchers to view both custom attributes and model attributes. For complex-enough user interfaces, you will start noticing that the UI becomes slower to render, diminishing user experience.
- It relies heavily on mutation. As functional programmers, we strive to limit side effects to a minimum.
The slowness that comes with this and similar approaches is two-fold. First, AngularJS and friends have to watch all of the properties of every model in the scope to track updates. Once the framework determines that data has changed in the model, it then looks up parts of the UI, which depend on information such as the fragments by using ng-model, and then it re-renders them.
Secondly, the DOM is the slowest part of most single-page web applications. If we think about it for a moment, these frameworks are triggering dozens or perhaps hundreds of DOM event handlers to keep the data in sync, each of which ends up updating a node—or several in the DOM.
Don't take my word for it, though. I ran a simple benchmark to compare a pure calculation versus locating a DOM element and updating its value to the result of the said calculation. Here are the results—I've used JSPerf to run the benchmark, and these results are for Chrome 37.0.2062.94 on macOS X Mavericks[2]:
document.getElementsByName("sum")[0].value = 1 + 2 // Operations per second: 2,090,202 1 + 2 // Operations per second: 780,538,120 Updating the DOM is orders of magnitude slower than performing a simple calculation. It seems logical that we would want to do this in the most efficient manner possible.
However, if we don't keep our data in sync, we're back to square one. There should be a way by which we can drastically reduce the amount of rendering being done, while retaining the convenience of two-way data bindin...