UI with ReactJS and ImmutableJS
The reader is assumed to be familiar with kent/src and its CGI programs, and have a working knowledge of Javascript.
Background
Evolution of Javascript in kent/src
Before JS: just the CGI
- complete page render for every click
- the browser's back button worked because each page's content was static
- simple top-down thinking about page content, but low performance to hit server and redraw entire page every click
JS inlined in HTML
- onClick="..." enabled some client-side actions... but escaping inlined javascript inside printf'd HTML strings gets ugly quick
- the browser's back button usually still worked as expected, but not always
AJAX and JQuery: manipulating the DOM
- using jQuery made it much easier to inspect and change the DOM
- using javascript files enabled real code to be written, not just inlined statements
- using jQuery supported some bad habits such as using DOM elements as global variable storage
- authors of javascript code tended not to adhere to kent/src coding standards... it got ugly.
- after JS changes the page, the back button behavior is almost never what you'd expect
MV*
Meanwhile, most of the Javascript / front-end community was working with a more structured paradigm: Model-view-controller (MVC). That separates roles into modeling the application in terms of data structures and logic, presenting a graphical view (think HTML / CSS), and responding to input from the user. Many libraries and frameworks arose to support MVC (or more broadly, MV*) architectures. TodoMVC offers a platform for investigating and comparing the more popular frameworks by hosting each framework's implementation of the same simple todo-list app.
An abandoned experiment
After hearing good things about MV* and BackBone.js from Brian Craft, and comparing several MV* frameworks' code in TodoMVC, I decided to build a dynamic single-page app using Backbone's Model and View classes. Based on an evaluation of templating systems conducted by LinkedIn's engineering team, I chose dust.js for generating HTML from templates (compiled to JS by dust).
I made a static page htdocs/hgAi/index.html, various dust templates for dynamically generated HTML, and model & view subclasses to implement clade/genome/db selection, position search w/autocomplete, and a list of sections for selecting and configuring data sources. At first I was very pleased by the separation of model code from view code, but as the app structure became more complicated, maintaining parallel collections of submodels and subviews that changed in response to UI events became rather complex and unwieldy.
Starting with a static file, and filling in everything including the nav bar after an initial ajax request, caused things to jump around unpleasantly on the page when starting up. We already have code in place to spit out the nav bar immediately; no harm in using it, just for the sake of avoiding any HTML written by C code.
ReactJS & JSX
Simplicity of top-down render, made efficient
React home page: http://facebook.github.io/react/
Video of JSConf presentation about the thinking behind React: https://www.youtube.com/watch?v=DgVS-zXgMTk (HT Brian Craft)
JSX: HTML-ish with the power of JS
http://facebook.github.io/react/docs/jsx-in-depth.html
Special methods of React components
When constructing a new React component, at a minimum you must provide a render
method that returns a React component object (e.g. a React div containing all of your cool elements). There are several other methods or properties with special meaning for React. For more info, see the React doc: http://facebook.github.io/react/docs/component-specs.html
React mixins
http://facebook.github.io/react/docs/reusable-components.html#mixins
The Flux architecture: why not?
React is awesome for rendering in the browser and receiving browser events, but it says nothing about how the rest of the system should be architected. It provides the V in MVC, but M and C are up to you.
The React team has been espousing an architecture called Flux: http://facebook.github.io/flux/docs/overview.html It focuses on a unidirectional flow of control and data. Instead of model and view, Flux has ActionCreator, Action, Dispatcher, Store and View. This division leads to several distinct objects for each type of data that will feed into the view. I find it harder to reason about than a model that passes data to the view and receives events from the view, and I'm not the only one: a significant portion of the questions on the ReactJS Google Group are not about React per se but are about Flux. Also, there are several subtly different implementations. There may be some advantages of isolating so many roles as objects for social apps with multiple asynchronous incoming streams of data, but for a user-driven querying interface, I think Flux is overly complex.
ImmutableJS
Video in which David Nolen describes the benefits of using efficiently implemented immutable data structures with React: https://www.youtube.com/watch?v=DMtwq3QtddY (HT Brian Craft)
- simplify reasoning about state changes; avoid bugs caused by out-of-sync mutable state
- very fast change detection (
===
instead of traversing objects) to avoid re-rendering when unnecessary - super-easy implementation of undo/redo
The new architecture
The new UI architecture is very simple: on the client (web browser), there is a model that is responsible for maintaining the entire state of the user interface, communicating with the server/CGI, and triggering the view to render, and a view that renders the model's state to the DOM and calls the model back when the user does something.
The CGI's role shifts from doing most of the rendering to acting as a back end for the UI. When the UI needs info from the cart, database, hub etc., it requests data from the CGI. When the user makes a change, the UI tells the CGI to update the user's cart (and possibly send refreshed data).
Bootstrapping: CGI prints minimal HTML, Javascript takes it from there
Instead of writing out HTML for the complete initial page display, the CGI now writes out just the toolbar, an empty <div>
to be filled in by React components, and <script src="...">
tags to include the necessary javascript files. For performance, it's also a good idea to add a <script>
that creates a javascript variable containing initial state so the the React code doesn't need to do an ajax request before it can display the initialized components.
The final javascript file instantiates the model, which in turn calls the view's top-level render method. Subsequent ajax responses or user actions invoke the model, which updates the state and in turn invokes the view to re-render.
See doMainPage in hgAi.c for an example.
ImModel: monolithic UI state model using ImmutableJS
The model's representation of the UI state is an immutable tree of objects, lists and scalars (strings/numbers). Every time the state changes in response to incoming ajax data or a user action, a completely new immutable data structure is created -- but thanks to the ImmutableJS library, this is done in a very space-efficient manner. This allows us to keep a stack of successive UI states, making undo/redo extremely easy to implement. Also thanks to the use of efficiently implemented immutable data structures, a simple ===
comparison will tell if there has been a change in any descendant of that node, so React components can avoid re-rendering if none of the state that affects them has changed.
After advancing to a new immutable state, the model calls the top-level React component's render function, passing in its immutable state data structure, and the user sees the updated UI.
ImModel is just a base class; an app model is a subclass of ImModel. ImModel provides a constructor function and several utility methods (e.g. registration and dispatching of handler functions, undo/redo, ajax requests, error reporting). Subclasses add handler functions for specific ajax response keys and UI events, and register those handlers in an initialize
method. Subclasses can also add mixins (more below).
Building an app model on top of ImModel
Handling user actions
To handle a particular user action such as clicking a checkbox or changing a selection from human to mouse, an app model defines a handler function and registers it with this.registerUiHandler
, a method provided by ImModel. The handler function takes one or two arguments: a path (i.e. a sequence of identifiers and optionally keywords that identifies the particular input/action) and, if applicable, the new value. For example:
var MyAppModel = ImModel.extend({ ... changeOrg: function(path, newOrg) { // Update the state data structure and notify the server }, ... initialize: function() { ... this.registerUiHandler(['org'], this.changeOrg);
When the user clicks a button or changes the value of an input, the view's React components call the model's update
method with a path and the changed value if applicable. The ImModel base class provides an update
method that dispatches to one or more handlers; it calls a handler if the incoming path matches the path with which the handler was registered.
Handling AJAX responses
ImModel dispatches ajax responses by the response object's top-level keys (no path). If no handler is registered for a key, the key and its value are added to the app's state. The app model can register handlers if more action is required when a particular item arrives from the server. For example:
handleCartVar: function(cartVar, newValue) { // Some updates have side effects... if (cartVar === 'trackDbInfo') { // Change default track too initialize: function() { ... this.registerCartVarHandler(['trackDbInfo', 'tableFields'], this.handleCartVar);
Suspension of immutability for performance (and easy undo)
It's common for an ajax response or user action to affect multiple page elements. For example, if the user changes database, not only the database select input but also the group/track/table options will change. That means that multiple parts of the immutable state tree will change in response to a single action that should correspond to a single undo/redo step. While ImmutableJS has an efficient implementation of immutable objects, its authors recognize that it's needlessly inefficient to create a series of 6 immutable objects for a single batch of 6 changes, so they provide an interface for making an object temporarily mutable. ImModel handles the details of this; the upshot for subclasses is that their handler functions modify the mutable incarnation of state, this.mutState
, and don't touch the actual state. For example:
this.mutState.set('species', newSpecies);
Undo and redo
ImModel mixins
View: React components using ImmutableJS
The view is a hierarchy of React components; at the leaves are strings of text or React virtual DOM elements such as div
, img
, input
etc. The render function returns a tree of React components constructed according to the immutable state data structure passed in from the model; in turn, those components' render methods are called if their part of the data structure has changed. Ultimately, React detects which DOM elements need to change, and changes only those.
Every component that has at least one possible user action receives two special properties: update
, a function for notifying the model of any change, and path
, a list of indices within the state data structure to the component's state, optionally followed by keywords to indicate the kind of action. Every user action has a corresponding callback which calls update
with a possibly augmented path
and optional data.
Avoid internal state when possible
Most view components have no internal state, and make no changes in direct response to user actions; they rely on the model to handle everything. One exception is TextInput; while the user is typing, instead of having a complete rendering cycle, it maintains its own state and notifies the model of the new value only when the user is done typing. Another exception is any component that uses a JQueryUI widget such as Autocomplete or Sortable; JQuery works by directly changing the DOM, so it completely avoids the model and React render cycle. When a widget completes an action, the view tells the widget to cancel its DOM changes and notifies the model of the change. Then the model triggers the usual React render.
Using UI state from ImModel
The model passes its immutable state data structure to the top-level React component's render method via the prop appState
. Since appState
is a tree of ImmutableJS objects (as opposed to plain old JS objects), it is traversed and accessed using ImmutableJS methods. This does bulk up the code a bit, and trying to access a plain old object property instead of using a method can result in silent bugs, i.e. you get undefined from the object property, which might be an acceptable result from using the method.
Below is an illustration of how plain old JS object dereferences translate to ImmutableJS methods. First, here are some example JS object dereferences:
// If we were using plain old JS objects for state (but we aren't!) var db = appState.db; var firstTrack = appState.trackDbInfo.trackList[0];
Here is how we use ImmutableJS methods instead:
// Here's what our React components need to do: var db = appState.get('db'); var firstTrack = appState.getIn(['trackDbInfo', 'trackList', 0]);
Often, a branch of the appState
tree will naturally correspond to a React component instantiation. In that case, it's appropriate to pass down just that branch as a prop instead of appState:
render: function() { return ( ... <CladeOrgDb menuData={appState.get('cladeOrgDb')} ...
Passing user actions to ImModel
When the user clicks on a button, changes a select input, etc., it's up to the model to change the UI state and trigger a re-render. The React component is notified of user actions by event callbacks:
<input type='button' value='Add' onClick={this.onAdd} />
In turn, the callback method needs to notify the model. This is done by calling the function this.props.update
with a path (list of property names and/or index numbers optionally followed by keywords) that uniquely identifies the UI element and the kind of action. Each component is instantiated with special props update
and path
, and uses those to alert the model. For example, for a click on the simple button above, the event handler calls update with a path that begins with the path passed down from above and ends with a keyword that tells the model what particular button was pushed:
onAdd: function() { // Send path + 'addThingie' to app model this.props.update(this.props.path.concat('addThingie')); },
Sometimes data needs to be extracted from the (React virtual) event object and passed along with the path. For example, when the user changes a select input (with full path passed down from above):
onChange: function(e) { var newValue = e.target.value; this.props.update(this.props.path, newValue); },
JQueryUi: oil & water with React, but still too useful to drop
The CGI speaks JSON now
One nice feature of Javascript is its object literal notation because it is able to express nested data structures reasonably compactly and, with pretty-printing, in a fairly human-readable form. Javascript data structures without loops or function references can easily be serialized into JSON, a more restricted notation that is easy to parse. That makes JSON an ideal interchange format for data that's not too big when working with Javascript and some other language, in our case C.
The UI model sends JSON-encoded requests to the server/CGI, and expects JSON responses. For example, if the user selects a different clade, the model formulates this command as a Javascript object:
{ changeClade: { newValue: "rodent" } }
js/model/lib/cart.js makes a request to the CGI with the param cjCmd
set to the CGI-encoded JSON string representing that object. Now the CGI needs to
- recognize the
cjCmd
param -- that means output will be JSON, not HTML or text - decode and parse
cjCmd
's JSON value into a C data structure - act on the command(s) as specified in the data structure and write out any reponse data as JSON.
The new lib module hg/inc/cartJson.h provides a function cartJsonExecute
that does part 2 (using src/inc/jsonParse.h) and also part 3 for common commands like changing clade/genome/database or retrieving track & table menu data (using hdb, cart, trackDb etc and src/inc/jsonWrite.h). (For a list of those common commands, see the source code for cartJsonNew
.) It also pushes & pops a warning handler that accumulates warning messages into a string that will be added to the response JSON, e.g. warning: "ftp server timed out etc..."
, that the UI can then present to the user or not. CGIs create a cartJson object, and can plug in their own part 3's by writing handler functions with this signature:
typedef void CartJsonHandler(struct cartJson *cj, struct hash *paramHash); /* Implementation of some command; paramHash associates parameter names with * jsonElement values. */
and then registering them with cartJsonRegisterHandler
.
Here is an example use of cartJson by a CGI that needs one app-specific command, getThingie. First the CGI defines a handler function:
static void getThingie(struct cartJson *cj, struct hash *paramHash) /* Write JSON for an object with some info */ { char *foo = cartJsonOptionalParam(paramHash, "foo"); // ... compute ... use cj->cart if we need the cart ... jsonWriteObjectStart(cj->jw, "thingie"); jsonWriteString(cj->jw, "title", fooDerivative); jsonWriteString(cj->jw, "url", etc); jsonWriteObjectEnd(cj->jw); }
Then when the CGI detects the special parameter cjCmd
, it creates a cartJson object, registers the handler function, and calls cartJsonExecute
to do the rest (including printing out the Content-Type header).
static void doCartJson() /* Perform UI commands to update the cart and/or retrieve cart vars & metadata. */ { struct cartJson *cj = cartJsonNew(cart); cartJsonRegisterHandler(cj, "getThingie", getThingie); cartJsonExecute(cj); } void doMiddle(struct cart *theCart) /* Depending on invocation, either respond to an ajax request or display the main page. */ { cart = theCart; if (cgiOptionalString(CARTJSON_COMMAND)) doCartJson(); else doMainPage(); }