Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Beyond Angular and Backbone with Undirectional apps

Beyond Angular and Backbone with Unidirectional apps

What is a unidirectional app?

Unidirectional is a term coined by React engineers to describe the data flow of an application. Unidirectional apps employ functional reactive programming techniques such as immutability, purity, and most importantly unidirectional (as opposed to bidirectional) data flow.

A unidirectional app is defined by no mutable references no two-way references between concerns.

Unidirectional app flowchart

Benefits over traditional bidirectional MV* apps

  • Everything has one source of truth, immensely improving code readability and reasoning.
  • Immutable state is debuggable. It's trivial to send a dump to server (with history) on exceptions and bugs.
  • Server-side rendering (without a headless browser). The code is - dare I say - isomorphic.
  • Time travel and undo out-of-the-box: Your entire app is one deterministic view (and serializable).

Using a virtual-dom to represent views you get:

  • Zero DOM manipulation in application code.
  • No templates.
  • Highly performant rendering.
  • Render to multiple targets, which the browser is one of.

Unidirectional vs. bidirectional MV*

Nobody can seem to agree on what MVC is, but popular MC* frameworks/libraries like Angular and Backbone have little separation between model, view, and controller.

Angular and Backbone both utilize bi-directional data binding. Views can manipulate state, controllers can manipulate state, models can manipulate state, and views directly manipulate the DOM (especially in backbone), often with a combination of direct DOM manipulation and templating.

Contrast this with unidirectional apps where views cannot modify state, but instead are a function of state. Views are vanilla JS functions that receive state and return a virtual DOM tree:

var h = require('virtual-hyperscript');

function view (state) {
  return h('span', state.message);

What about input like click events? In bidirectional MV* apps, click events are usually handled in the view (Backbone comes to mind), and manipulate the local view state. In unidirectional apps, the view only declares the events that it should delegate. Event delegators and handlers are separated so that user input cannot change the view only the state, resulting in a unidirectional data flow: Input -> State -> View instead of View <--> State.

Undirectional views and Virtual DOM

Unidirectional views are based on the idea of a "Virtual DOM" popularized by React. Instead of views manipulating the DOM directly, they describe the UI for any given state and this "virtual DOM" is diffed with the previous one to produce the minimal set of DOM operations necessary to repaint the page.

Virtual DOM sounds slow, right? Recreating the entire tree on every state change? Quite the contrary. This technique is widely used in the game industry and performs just as well for DOM updates.

Virtual DOM framework performance comparison

What does a virtual DOM look like?

var h = require('virtual-hyperscript');

var tree = h('', [
    h('span', 'some text'),
    h('input', { type: 'text', value: 'foo' })

The virtual DOM is expressed as a tree of objects in vanilla javascript. Templates become unnecessary while preserving the separation between views and business logic. DOM manipulation is made irrelevant, being treated as nothing more than a render target. Developing and reasoning about an application becomes much easier as DOM manipulation is no longer a concern for the developer.

Immutable state

State in unidirectional apps is expressed using immutable data structures. Immutability prevents views from polluting state, enables easy observation of state changes, and allows for performance optimizations through thunkifying/caching of views.

Because state is immutable views become pure functions. Given a certain state as input, we are guaranteed to have a certain output for every function call. One of the neater benefits of this is easy time-travel and undo. For each change, a state can be saved and the entire app can be rendered at any given point by rewinding the state. For example, you could trivially replay a user's entire session with your application!

Tying it all together

An app defines its top level state "atom" (model). Events are wired up to Input which define updates to the state (controller). Finally, rendering logic is a single function that takes the entire state of the application and returns a virtual DOM representation of the UI (view). Every time state changes, the virtual DOM is recreated and diffed with the previous one to update the DOM.

Data flows in one direction (unidirectional) from Input -> State -> View.

Building unidirectional apps with Mercury

The concepts described in this article are being explored by Mercury: A modular front-end framework for building unidirectional apps. It consists of a set of modules that provide all the building blocks for a unidirectional app, and it's faster and leaner than React / Om / ember+htmlbars in multiple benchmarks! Unlike React, it does not couple the virtual-dom implementation with the component implementation.

Why React (and Flux) is not enough

React and Flux do not enforce immutability. Developers are supposed to implement their own .shouldComponentUpdate(), determining in their application code whether a component should update, instead of a state change strictly determining the update. Components can also keep local state! This makes it impossible for an app to be a single deterministic view composed of sub-views. Luckily, there are frameworks which add immunitability and replace .shouldComponentUpdate() to work with a global state atom. Morearty and Omniscient are two such examples, similar to ClojureScript's Om.

Compose virtual DOM with JSX

For those that would prefer describing the virtual DOM in HTML like they did in templates, JSX desugars XML into virtual DOM:

var h = require('virtual-hyperscript');

var tree = <div id="some-id">
  <span>Some text</span>
  <input type="text" value="foo" />

is desugared to:

var h = require('virtual-hyperscript');

var tree = h('', [
    h('span', 'some text'),
    h('input', { type: 'text', value: 'foo' })

Recommended reading


This comment has been minimized.

Copy link

@Raynos Raynos commented Sep 6, 2014

Comments from IRC:

4:07 PM dreamdust: i'll read soon, I was also thinking of starting a blog on the unidirectional github account
4:07 PM a shared blog would be really awesome for this kind of stuff, wanted to do an article about dom-delegator
4:32 PM dreamdust: i think the intro to reactive programming is the wrong thing to link for "functional reactive"
4:34 PM dreamdust: link to instead
4:35 PM dreamdust: drop the three bullet points into "No mutable references and no two way references between concerns"
4:35 PM dreamdust: > Benefits over traditional bidirectional MV* apps
4:35 PM - your immutable state is debuggable, trivial to send a dump to server (with history) on exceptions and bugs
4:36 PM - everything goes through well defined boundaries of concerns, easy to introspect where performance bottlenecks are.
4:36 PM - (MOST IMPORTANT) everything has one source of truth, very easy to read code
4:37 PM dreamdust: read
4:38 PM dreamdust:
4:38 PM dreamdust:
4:38 PM dreamdust:
4:40 PM - write real declarative UI code.
4:40 PM - use real higher order functions and real module system for templates
4:41 PM Sorry I split my list of things to add to benefits in the middle with links to other talks and article :)
4:43 PM dreamdust: "Immutable enables easy observation of state changes" this is not true. its the observable state that allows this. the observ-struct and pals are both immutable & observable
4:43 PM dreamdust: " One of the neater benefits of this" the neatest benefit is testing, such test, so easy, such pure, so reproduce.
4:44 PM dreamdust: the rest looks ok


This comment has been minimized.

Copy link

@azat-co azat-co commented Jul 18, 2015

Nice overview!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment