levand.quiescent

https://github.com/levand/quiescent.git

git clone 'https://github.com/levand/quiescent.git'

(ql:quickload :levand.quiescent)
609

Quiescent

A lightweight ClojureScript abstraction over ReactJS, emphasizing its ability to (re)render immutable values efficiently.

An obligatory TodoMVC implementation is available.

See the documentation for instructions and examples of how to use Quiescent.

Rationale

ReactJS is an extremely interesting approach to UI rendering for HTML-based web applications. Its core value proposition is to make it possible to write one set of UI rendering code that works for both initial creation and any future updates. Updates are extremely efficient, because only the minimal set of necessary deltas are actually sent to the DOM.

In other words, React lets you write your UI code once, and still have a dynamic, data-driven application. Ideally, it eliminates any need to write explicit UI manipulation code. Instead, you are responsible only for supplying a specification of what the UI should look like, given certain data. The ReactJS implementation takes care of all the details of how to morph the existing DOM to correspond to changes in the input data.

Quiescent is intended to expose this feature and only this feature in a way that is convenient and idiomatic to ClojureScript, while remaining highly efficient.

It has the following design goals:

This is intended to emulate classical ‘rendering’ semantics for 2D or 3D bitmapped applications, where the entirety of a scene is re-drawn from scratch for every frame. Of course, React/Quiescent does not actually do this, for performance reasons, but the fact that it does not is an implementation detail; the conceptual model is the same.

These goals differ slightly from other ClojureScript interfaces to React, as described below.

Comparison with Om

Om is another ClojureScript interface to ReactJS, highly capable and well-designed. It provides categorically more features than Quiescent, at the cost of taking more control and specifying more rigidly the way application state is modeled, updated and re-rendered.

The most important conceptual distinctions are:

Specifically, addressing the two reasons David Nolen gives for allowing application state:

  1. “Local state pollutes application data.” An alternative view is that all data is application data, whether it is transient or persistent, important or insignificant, wherever it is actually located. There is little harm in including it in the top-level data structure, and if your application does demand a hierarchy of data (persistent vs. transient, etc) for different purposes, it is better to model that relationship explicitly rather than leaving transient data tucked invisibly away in component state.

  2. “Local state is always up-to-date.” This is not relevant in Quiescent's render model because Quiescent does not manage state.

  3. Om components are always aware of their location in the primary application state, via a cursor (a hybrid of functional zippers and lenses). Quiescent components are not. This means that Om components can dispatch updates to “themselves”, whereas a DOM event handler function attached to a Quiescent component can only effect change by reaching back and doing something to the top-level application state (e.g., by sending a core.async message, swapping the top-level atom, etc).

This does, somewhat, negate the concept of component modularity; Quiescent's contention is that the benefit of top-down, value-based rendering exceeds that of truly modular components.

Ultimately, though, Om is an excellent, well-thought-out library, and if your needs or design goals more closely align with its capabilities than with Quiescent, you should absolutely use it.

Comparison with Reagent

Reagent is another ClojureScript wrapper for ReactJS. It is, perhaps, easier than either Om or Quiescent and certainly the most readable of the three.

The key differences between Reagent and Quiescent (or Om, for that matter) are:

Although I do not have as much first-hand experience with Reagent, it seems to be a very convenient approach, and if it meets your needs you should definitely give it a try.

Implementation

This section presumes familiarity with how ReactJS works.

In short, basic Quiescent components implement only two of ReactJS's component lifecycle events:

  1. shouldComponentUpdate is always predicated exclusively on whether the immutable value passed to the component constructor function has changed. If so, the component re-renders. If not, it doesn't.

  2. The implementation of render is provided as a function supplied by the user. The output of this render function is presumed to be a single ReactJS component, as it is in vanilla ReactJS. The function, however, is passed the immutable value that was used to construct the function. It is also passed any additional arguments that were provided to the component constructor.

See Also

CHANGE LOG

0.2.0

Warning: This release contains breaking changes.

motivation for wrapper deprecation

As it turns out, there is no way in the current model that wrappers can provide the desired functionality in all cases.

A wrapper component can only modify its own lifecycle methods, not truly those of its child. But neither can it access the “shouldComponentUpdate” of the child - it must have a “shouldComponentUpdate” that constantly returns true. Therefore, an “onRender” wrapper would fire even if the wrapped component explicitly did not render due to an unchanged value (or otherwise overriding “shouldComponentUpdate”).

0.1.2

License

Copyright © 2014-2015 Luke VanderHart

Distributed under the Eclipse Public License (see LICENSE file).