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.
- 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.
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
.
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.
What does a virtual DOM look like?
var h = require('virtual-hyperscript');
var tree = h('div.foo#some-id', [
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.
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!
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.
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.
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.
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" />
</div>
is desugared to:
var h = require('virtual-hyperscript');
var tree = h('div.foo#some-id', [
h('span', 'some text'),
h('input', { type: 'text', value: 'foo' })
])
Nice overview!