UI with ReactJS and ImmutableJS: Difference between revisions

From Genecats
Jump to navigationJump to search
(→‎Passing user actions to ImModel: Event handlers, update, path, optional data.)
(→‎The new architecture: Moved some intro text into subsections.)
Line 65: Line 65:
= The new architecture =
= The new architecture =


The new architecture is very simple: there is a model that is completely responsible for maintaining the state of the user interface and communicating with the server/CGI, and a view that renders the model's state and calls the model back when the user does something.  The CGI, instead of printing out complete HTML for the page, simply prints an empty <code>&lt;div&gt;</code> that will be filled in by React.   
The new architecture is very simple: there is a model that is responsible for maintaining the entire state of the user interface and communicating with the server/CGI, and a view that renders the model's state to the DOM and calls the model back when the user does something.   


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 <code>===</code> 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 their state has changed. 
== Bootstrapping: the CGI prints minimal HTML ==


The view is a hierarchy of React components; at the leaves are strings of text or React virtual DOM elements such as <code>div</code>, <code>img</code>, <code>input</code> etcEvery component that has at least one possible user action receives two special properties: <code>update</code>, a function for notifying the model of any change, and <code>path</code>, 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 <code>update</code> with a possibly augmented <code>path</code> and optional data.
Instead of writing out HTML for the complete initial page display, the CGI now writes out just the toolbar, an empty <code>&lt;div&gt;</code> to be filled in by React components, and <code>&lt;script src="..."&gt;</code> tags to include the necessary javascript filesFor performance, it's also a good idea to add a <code>&lt;script&gt;</code> 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.


After advancing to a new immutable state, the model calls the top-level React component's render function, passing in its immutable state data structureThe render function returns a tree of React components constructed according to the state data structure; in turn, those components' render methods are called if their part of the data structure has changedUltimately, React detects which DOM elements need to change, and changes only those.
The final javascript file instantiates the model, which in turn calls the view's top-level render methodSubsequent ajax responses or user actions invoke the model, which updates the state and in turn invokes the view to re-render.   


== Bootstrapping: the CGI prints minimal HTML ==
See doMainPage in [http://genome-source.cse.ucsc.edu/gitweb/?p=kent.git;a=blob;f=src/hg/hgAi/hgAi.c hgAi.c] for an example.


Instead of writing out HTML for the complete initial page display, the CGI now writes out just the toolbar, <code>&lt;script src="..."&gt;</code> tags to include the necessary javascript files, and an empty <code>&lt;div&gt;</code> to be filled in by React components.  For performance, it's also a good idea to add a <code>&lt;script&gt;</code> 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.
== ImModel: monolithic UI state model using ImmutableJS ==


See doMainPage in [http://genome-source.cse.ucsc.edu/gitweb/?p=kent.git;a=blob;f=src/hg/hgAi/hgAi.c hgAi.c] for an example.
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 <code>===</code> 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 their state has changed.


== ImModel: monolithic UI state model using ImmutableJS ==
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.  The render function returns a tree of React components constructed according to the state data structure; 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.


=== Handling user actions ===
=== Handling user actions ===
Line 91: Line 91:


== View: React components using ImmutableJS ==
== 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 <code>div</code>, <code>img</code>, <code>input</code> etc.  Every component that has at least one possible user action receives two special properties: <code>update</code>, a function for notifying the model of any change, and <code>path</code>, 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 <code>update</code> with a possibly augmented <code>path</code> and optional data.


=== Avoid internal state when possible ===
=== Avoid internal state when possible ===

Revision as of 00:36, 17 January 2015

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 immutable data structures with React: https://www.youtube.com/watch?v=DMtwq3QtddY (HT Brian Craft)

The new architecture

The new architecture is very simple: there is a model that is responsible for maintaining the entire state of the user interface and communicating with the server/CGI, and a view that renders the model's state to the DOM and calls the model back when the user does something.

Bootstrapping: the CGI prints minimal HTML

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 their state 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. The render function returns a tree of React components constructed according to the state data structure; 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.

Handling user actions

Handling AJAX responses

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. 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

  1. recognize the cjCmd param -- that means output will be JSON, not HTML or text
  2. decode and parse cjCmd's JSON value into a C data structure
  3. 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();
}