An Engineer's Overview of Backdraft

Backdraft is a minimal, small, self-contained, pure JavaScript library for building browser-hosted user interfaces that minimizes the construction complexity of those interfaces.

The Backdraft Programming Model

Backdraft facilitates building browser-hosted user interfaces:

Backdraft is a library for building classes that present a public API that is engaged by the computational component and contain a private implementation that manipulates the DOM to realize the UI.

This is very different than many popular alternatives. If you think otherwise, I challenge you to go to reactjs.org and find a single example that publishes a well-defined public API that can be engaged by an independent computational component.1 Indeed, all the examples have the computational component and UI components entangled.

A UI doesn’t actually accomplish (compute) anything: it shows output and collects input. Period. Dot! Indeed, a program that contains only a user interface has zero value. It is the computational component of a program that has value and the UI component is nothing more than a conduit to that computational component. Further, the variety of computational components is limitless, and the design and implementation of different computational components are often quite diverse. Yet, even when building programs for vastly different computational components, if the UIs are realized by manipulating a DOM tree then those UIs can share a common design and implementation. This is a key observation:

If different UIs are realized by manipulating the DOM tree in a browser then they can share a common design and implementation irrespective of the computation components they are serving.

This observation is perhaps the key motivator to the design of Backdraft. Backdraft intends that UIs are designed and implemented independent of the computational components they serve (of course both components can exist in the same program). This intention has a consequence:

There must be a well-defined interface between the computational component and the UI component.

Backdraft fulfills this requirement with the canonical object oriented design model: the construction of software as a collection of data types2. Here is a complete example from the Backdraft tutorial:

export default class HelloWorld extends Component {
   get language(){
      return this._language || null;
   }

   set language(value){
      this._language = value;
      if(this.rendered){
         this._dom.root.innerHTML = this._getTranslation();
      }
   }

   _elements(){
      return e("div", this._getTranslation());
   }

   _getTranslation(){
      return this.kwargs.translations[this._language] || "hello world";
   }
}

The public API is obvious: a getter/setter that describes the current language. Clients use this API on a logical level (setting a language), but are completely insulated from the details of how the UI is actually manipulated. Implementers of the HelloWorld class concern themselves with manipulating the DOM—we shall see how Backdraft simplifies this job in a moment—but are completely insulated from how the class may be used. There is a crystal-clear line drawn between these concerns. And drawing this line simplifies everything: designing, implementing, and testing.

Client Interface Implementation Features

To begin, Backdraft UI components take the form of a JavaScript class intended to contain a public interface and private implementation. So the idea of providing a public interface to a computational component is provided by design. Component instances are visible and their APIs may be exercised.3 Backdraft includes the concept of parent-child relationships among components and provides automatic lifecycle management within hierarchies of component instances.

Additionally, UI classes built with Backdraft include machinery to signal events. Such events are almost never a reflection of DOM events, but rather domain-defined events that form part of the public API of a UI component. Similarly, there is machinery to signal instance property mutation. These capabilities are important when building public interfaces.

User Interface Implementation Features

Building a browser-hosted UI requires building DOM trees; therefore, any library that aims to build browser-hosted UIs must embrace this problem. Backdraft provides for expressing DOM trees in the most frugal language possible: a language that uses JavaScript to provide declarative composition. Backdraft element trees are used to describe the DOM that is rendered to create a UI component.4 The elements that comprise these trees can indicate simple DOM nodes and/or Backdraft component classes, thereby allowing complex UI classes to be built by composing simpler UI classes.

This design is minimal and self-contained in many ways:

  • There is but one technology, JavaScript, to learn. No additional languages or tools are required.
  • There is a single word that is defined by Backdraft, the element factory e(), that defines arbitrary complex trees.
  • There is no intermediate machinery (e.g., event systems that purport to improve the native event system) between the defined components and the DOM.

Once a UI implementation gets to the point of directly manipulating the DOM, Backdraft embraces simple and direct access to DOM nodes.

Simple yet powerful APIs are provided for pervasive UI behaviors like being visible or not, enabled or not, tab-indexable or not, detecting focus changes, detecting size and scaling changes, and className mutations. However, intentionally, there is no attempt to build in every possible UI feature in the core library: this design decision keeps the library focused as a foundation to all potential UI problems, thereby controlling complexity. Owing to its canonical OO design, hierarchies of UI classes can be built or the library can be extended directly to solve specific problems.

Because the library uses only modern JavaScript, there is never a requirement to compile, transform, build, or package the code. The library allows the traditional edit, load, debug cycle. And since there is no transformation of the code, the code in the debugger is exactly the same code in the editor. The library does not employ either the “state change => rerender” design or a virtual DOM implementation. Therefore, components constructed with the library are as performant in time and space as components written directly with no library support.

The library is small, about 1000 lines of pure JavaScript. It is easy to grok. The library has no dependencies; it is completely self-contained.

Further Discussion and Examples

In terms of a browser-hosted UI, ultimately, all output is realized by placing DOM nodes in the document and all input is realized by reacting to an event signaled by a DOM node. Therefore, the problem of implementing a UI in the browser always boils down to the problem of defining and manipulating DOM trees. For simple UI classes, this can be as simple as defining a tree with a node or two. But UI classes are often complicated. For example, a generic dialog box has many parts—a frame, a top banner with a title and a close button, a content area, and an area at the bottom that contains one or more buttons. Such a class should be able to stand on the shoulders of other classes and use, for example, TitleBar and Button components in its definition. Consider the following element trees which could be used to define Button and DialogBox classes with Backdraft.

// A Button
_elements() {
   return e("div", {
         className: "button",
         [e.advise]: {"click": this._onClick.bind(this)}
      },
      e("div", {[e.attach]: "_labelNode"}, this._label)
   )
}
// A Dialog Box
_elements() {
   return e("div", {className: "bd-dialog"},
      e("div", {className: "inner"},
         e(TitleBar, {title: "this.kwargs.dialogTitle"}),
         e("div", {className: "body"}, this._content()),
         e("div", {className: "buttons"},
            e(Button, {
               label: "OK",
               [e.advise]: {click: this.accept.bind(this)}}),
            e(Button, {
               label: "Cancel",
               [e.advise]: {click: this.cancel.bind(this)}})
      )
   ));
}

There are several powerful ideas in these examples.

First, the DOM trees are described using declarative expressions. Each node is described by its type, properties, and children. We know there is no easier way to define a tree because the “language” used, namely the Backdraft element factory function e(type, properties, children), conveys the minimal information of what is being described and nothing more.

Second, notice that the nodes can be either HTML elements or other Backdraft classes. This allows components to be abstracted and then more complex components be built by composing the abstracted components. These first two features—declarative expressions and composition—are defined as “declarative composition”.

Third, the language is JavaScript. There is no templating, markup, or hybrid language to learn, and, therefore, no preprocessor to install, learn, execute, and debug. An entire layer of complexity is eliminated. Further, since the language is JavaScript, expressing certain kinds of DOM trees can be done with exceptional frugality and clarity:

_elements() {
   return e("ol", {className: "todo-list"},
      this.list.map(item=>e("li", item))
   )
}

Is there any doubt in your mind about the structure of the DOM tree the code above describes? This is not a sketch: it is completely valid in a Backdraft class.

Fourth, when the implementation gets to the point of manipulating DOM nodes, Backdraft embraces the DOM API, intentionally providing easy access rather than trying to hide the DOM. There are no middle layers—for example, custom event systems. Properties and attributes can be set on DOM nodes when they are rendered. And when the UI component requires mutating node properties/attributes during the lifetime of the component, target DOM nodes are readily available. Extendable post-processing capabilities are provided through meta-instructions. Examples include [e.advise] which allows any event(s) defined by a node (whether a DOM node or a Backdraft component) to be connected to a handler(s); [e.attach] causes a reference to a node--DOM node or Backdraft component instance--to be recorded in an instance property; and [e.watch] causes a watcher to be set up on an instance variable.5

It’s important to point out that, when the element is a DOM node, [e.advise] connects directly to the native DOM event. Backdraft contains no middle layer that “improves” the native DOM APIs. In fact, separating the native DOM API from the programmer in a general-purpose UI library such as Backdraft causes several problems: (1) the programmer must learn the middle layer API and its consequences on the native API (more complexity); (2) the cost of any middle layer is never zero in terms of time and space; (3) the middle layer must be maintained to stay current with changing native DOM APIs (hard in the world of evergreen browsers); and (4) when the middle layer inevitably fails to provide some feature available on the native DOM API, the programmer must escape the middle layer (yet more complexity) and access the native API directly anyway. The point is not that middle layers that improve expressive efficiency or normalize several environments are bad, but rather that they solve an orthogonal problem to a UI construction library and therefore should be separate machinery. Other libraries do not take this approach.

Backdraft provides simple but powerful APIs for pervasive UI behaviors like being visible or not, enabled or not, tab-indexable or not, detecting focus changes, detecting size and scaling changes, and className mutations. For example, an extremely common problem is adding a CSS class to a node’s className when it enters an “error” state. Here’s an example of one way to accomplish that in Backdraft.

check(){
   // compute the validity of the current input;
   // store the result in this.error
   // ...

   this[this.error ? "addClassName" : "removeClassName"]("error");
}

And computing/getting/setting a component’s className works whether or not the component is rendered!

Backdraft does not re-render the DOM tree—virtual or otherwise—to effect changes to the tree. This is a vital difference when compared to some other frameworks. To require a complete re-render of a component instance—and, therefore, each contained component instance recursively down to every leaf—any time the least little thing changes in a DOM tree is a foolish waste of resources that leads to performance problems as well as increases the complexity of downstream components.

Let’s ignore the performance problem for a moment and think about the complexity implied by the “state change => rerender” design. At first blush, it would seem that each component need only be concerned with its own state with such a design. But this is not the case! Consider the case of a component that contains a tree of other components. We’ll call the top component the “parent” component and the contained components children, grandchildren, and so on. If a parent component rerenders every time its state changes, then the parent must ensure that it manages enough state for the decedent component(s) to ensure that their state before the rerender (which may have changed since the last render of the parent component) is properly communicated during the rerender. Of course, this complexity isn’t always a problem. But it’s never a problem—by design—when the design doesn’t force a rerender. Instead, Backdraft leaves it to the programmer to decide how best to effect change in a rendered DOM tree. Backdraft gives a choice: Backdraft component implementations can rerender on state change, but it is unusual for that technique to be optimal.

In fact, most DOM manipulations after the initial rendering are trivial. My experience is that the vast majority of DOM manipulations after the initial rendering fall into three categories:

  1. Change the className of a node.
  2. Change the content of a TEXTNODE.
  3. Insert/delete/reorder a set of child nodes that represent independent functionality.

Backdraft provides explicit APIs for each of these problems, even further devaluing the supposed utility of the state change => rerender design.

The state change => rerender design is mostly responsible for library implementations that include a virtual DOM. Remove this design requirement and a virtual DOM is unnecessary. This is a good thing because the virtual DOM is expensive. First, compared to direct manipulation of the DOM, employing a virtual DOM is at least an order of magnitude more complex in both time and space by definition.6 But there are other, less obvious, perhaps more important costs. Debugging code built on a library that employs a virtual DOM can be complicated since the user code actually manipulates the virtual DOM, not the real DOM, and reconciling the two is neither trivial nor instant. Consequently, tracking down a problem—whether a real library bug or a programming mistake owing to misuse of the library—is more difficult. Ask yourself, at 9:00 P.M. the night before delivery when something isn’t quite working correctly, which library would you rather step through: a few hundred lines of code that directly manipulates the DOM or thousands of lines that manipulate a hidden, undocumented structure that is magically and asynchronously reflected to

Next Steps

If the claims in this article appear to have value, please take a look at the tutorial (http://backdraftjs.org/tutorial/1-getting-started.html) for further details. You can also inspect, run, and play with several working examples in the Github Backdraft-tutorial (https://github.com/altoviso/backdraft-tutorial) project. If you have questions or comments, please drop me a line at rgill@altoviso.com. I look forward to your feedback.

End Notes

1I will compare and contrast some key points to React. But before I do that, I want to make it clear that I don’t think React is bad or defective. Indeed I’ve used React with great success on various projects. I’ve also used React components in projects constructed with Backdraft: pulling a well-written, off-the-shelf component as part of a solution to a bigger problem is often less expensive and always less time consuming than building it from scratch. All that said, and although some Backdraft ideas are similar to React ideas (e.g., declarative composition), Backdraft is significantly and purposely different than React and other frameworks of similar design. React’s design and ideas just happen to be so well-known that it serves as a good example to push against.

2Canonical object-oriented design is often poorly described and almost never formalized. The first four chapters of Object-oriented Software Construction (1st Ed; the 2nd Ed loses focus at times) (http://a.co/0XlsxHv) has a decent explanation of the issues. If you are looking for very formal treatment, try “Subtyping and Inheritance” in Databases, Types and the Relational Model (https://www.dcs.warwick.ac.uk/~hugh/TTM/DTATRM.pdf).

3This is not so in some other frameworks. For example, on the one hand, interfacing with component instances is unnatural, unusual, and considered mostly orthogonal to the programming model promoted by React. On the other hand, the React community recognizes that such direct access is required sometimes. Indeed, they've recently added a new API to support references in 16.3. But this begs the question: is the model defective when work-arounds to the intended model are required?

4Backdraft UI classes can also be implemented in terms of element forests; there is no requirement that a particular UI class define its interface in terms of a tree.

5There are other meta instructions and the meta instruction machinery is open to the programmer so that meta instructions can be added.

6It is indisputable that designs that employ a virtual DOM model are less performant than designs that directly manipulate the DOM, assuming proper implementations of both. And there is recent history that suggests the problems have historically caused enough trouble that major redesigns and revisions were accomplished (recently, React, released a new render engine to attempt to address performance problems). Of course in many cases, the virtual DOM model is fast enough. But at a minimum, when it isn’t fast enough—think large, dynamic grids—the only alternative is to throw out the work that’s been done with the slow design and start again with something else. If there is no cost in choosing the fast design in the beginning, then isn’t it better to have any single software system use a single design?