Writing large JavaScript projects is hard. The language lacks both a type system and a canonical module system, it has a rather clunky syntax and has a threading model that is, to say the least, interesting. No wonder so many third party JavaScript frameworks exist. Luckily JavaScript is a flexible language, which makes extending it easy.
As the Silk client keeps growing, keeping our JavaScript code base clean has proven to be a bit of a challenge as well. One of the techniques we use to keep the complexity manageable is Functional Reactive Programming (FRP). Reactive programming allows us to declaratively set up bindings between data in different parts of our product that keep in sync automatically. Most dynamic parts of our user interface are reactively connected to our underlying data model. FRP allows us to abstract over information that changes over time with minimal effort.
Currently we solely use reactive programming within our client application. Although not unthinkable (or even uncommon) we have no reactive connection from the client to the server (yet).
At Silk we developed our own JavaScript library for reactive programming. The interface is heavily inspired by functional programming idioms from Haskell. Prototyping the library in a type safe manner in Haskell gave us great confidence about the expressiveness of the programming interface.
Reactive Variables
At the basis of our library we can find reactive variables, mutable reference cells containing a value that might change over time. Value changes within a variable can easily be observed by other reactive variables, or by listeners that can perform arbitrary side effects.
We illustrate the basic usage of reactive variables below. We create a variable containing a number and inspect the current value by using the snapshot method get
. We can write a new value to the variable with set
, after which we can observe a new value. Pretty straightforward so far.
// Creating a variable with initial value:var v = newReactive(20);// Snapshot the value using 'get':console.log("v:", v.get()); // prints 20// Update the value using 'set':v.set(10);console.log("v:", v.get()); // now prints 10
Reactive variables can be observed by other reactive variables by linking them using the link
method. After linking two variables their values will keep in sync. Links can be broken with the unlink
method. Once a value is linked, so it depends on another value, setting it manually makes no sense anymore.
// Create two variables and link one to another:var a = newReactive(20);var b = newReactive(10);b.link(a); // b.get() => 20// Setting variable 'a' will now update the value of 'b' as well:a.set(4); // b.get() => 4// Unlinking cleans up the connection:b.unlink();a.set(3); // b.get() => still 4
Linking variables one-to-one can be useful, but isn’t the most interesting of use cases. In the section about composition we’ll see some combinator functions that link variables in more interesting ways.
Side Effects
To be able to perform actions when the value of a reactive variables changes, we can attach listeners. We use the function onchange
to register a side effect.
var myTitle = newReactive("<untitled>");// Install the side effect (using ECMAScript 6 lambdas!)var eff = myTitle.onchange((t) => document.title = t);// Setting the reactive value will now automatically// update the document.title as well:myTitle.set("MyDocument-1");// ...after a while, when no longer needed:eff.cleanup();
Note that if you set the value of a reactive variable to the value it already contains, no value propagation will happen and no side effects will be fired. Side effects only fire when the value changes or during initialization. The onchange
method returns a handle that can be used to cleanup the effect when no longer needed.
The onchange
method is really only meant to connect a value (or network of values) to some part of the outside world (like the document title), not to update other variables. Updating other variables is better done using link
or one of the composition combinators described below.
Reactive jQuery
As part of our framework we’ve built a small plug-in for jQuery that allows us to connect reactive variables to the DOM. We extend jQuery with methods similar to existing jQuery methods, but prefixed with the letter r
, indicating their reactive counterparts.
For example, rText
allows us to connect a reactive string to the text contents of a DOM element.
var header = newReactive("initial string");$("h1").first().rText(header);// Setting the reactive value now automatically updates// the text contents of our header element:header.set("New Header Text!");
Besides rText
, we have rHtml
, rAttr
, rAddClass
, rToggle
, and several variants. Instead of taking normal strings, HTML fragments, or booleans as input, the reactive counterparts take reactive variables containing strings, HTML fragments, or booleans as input. When the values change the DOM will update automatically. Additionally (with the use of mutation observers) we were even able to build a connection the other way around: when the DOM updates the connected variables also update.
Adding more functionality as jQuery plug-ins is easy and can be done with the use of the onchange
method. For example, to get the value of an <input>
element as a reactive value we could write:
// Extend jQuery with reactive input field values:$.fn.rVal =function (rv) // reactive variable as input
{var el = this;// When the input text changes, update the variable:el.on("input", () => rv.set(el.val()));// When the variable changes, update the element:rv.onchange((v) => el.val(v));return el;
};
Now instead of jQuery’s val
method we can use our own rVal
method.
var myValue = newReactive("");$("form input").rVal(myValue);// We can use the variable to interface with the input value:myValue.set("new value");
Why would we want to use reactive variables instead of using jQuery directly? Because we can easily observe changes and compose them with other reactive variables!
Composition Using Lift
Reactive values become interesting when you start composing them, building up data flow networks that automatically propagate information. The easiest way of composition in our framework is by lifting regular JavaScript functions into reactive functions. Reactive functions are functions that can be applied to reactive values and will return a reactive variable.
Let’s say we have a JavaScript function to compute the length of the diagonal of a rectangle, given the width and height. We can make a reactive function out of this by using Reactive.lift
.
var diagonal = Reactive.lift((w, h) => Math.sqrt(w * w + h * h));
Now we can apply it to reactive numbers instead of normal JavaScript numbers and get back a reactive number again.
// Create a rectangle with reactive properties:var myRect =
{ width : newReactive(20)
, height : newReactive(4)
};// Put the diagonal on our rectangle as a reactive computation// over the dimensions:myRect.diagonal = diagonal(myRect.width, myRect.height);// Link it to the text of some DOM element:$("div.diagonal").rText(myRect.diagonal);// The text on the page will now automatically update when// changing the dimensions of our rectangle!myRect.width.set(200);myRect.height.set(2);
The lifted functions will be re-run every time one of the input variables update. Because it is unclear when the function will run and even how many times it will run it must be kept pure. Side effects in lifted functions can cause strange and undefined behavior.
In the simple case of lifting a single argument function, we can use the method map
available directly on reactive values.
var numbers = newReactive("1.0 30e3 20.07 50");// Create a reactive view on the string as a list of actual// numbers. Because we lift a one argument function we can// just use 'map'.var asList = numbers.map((s) => s.split(/\s+/).map(parseFloat));
The lift
function is probably the mostly used combinator in our framework, because it allows us to create views on data that will automatically update when needed. These views are reactive variables as well, which makes attaching side effects and further composition easy.
Why is the map
function called map
?
The functions is called map
because it corresponds directly to the Haskell function fmap
, part of the Functor
type class. Similarly, the function lift
corresponds somewhat to a multi-argument version of Haskell’s liftA
(and liftA2
, liftA3
, etc) for the Applicative
type class. For those with a Haskell background we show the exact correspondence below.
// JavaScript: -- Haskell:lift(f)(a, b, c) liftA3 f a b clift(f)(a, b, c) f <$> a <*> b <*> c// JavaScript: -- Haskell:lift(f)(a) f <$> alift(f)(a) fmap f aa.map(f) fmap f a
Note that a unary lift
is equivalent to map
.
Nested Reactive Variables
What happens when a function lifted with lift
itself returns a reactive variable? The result of lifting will now be a reactive variable containing as the value another reactive variable. This is most likely not what you want.
To flatten a nested reactive variable we can use the join
or bind
methods.
// Say 'v.inner' is reactive, we need 'join' to flatten afterwards:var flattened = outer.map((v) => v.inner).join();// Bind is simply a shortcut for 'join' after 'map':var flattened = outer.bind((v) => v.inner);
To illustrate how this is useful we take a small example directly from the Silk product. In our client we allow users to edit Silk pages, which are internally called documents. Every document contains a list of properties, like the title, the main article node and some more state variables. Some of those properties (like the title) are reactive, because we want an easy way to observe their changes.
The Silk client also has the concept of a canvas, an object that can render Silk documents to the screen. The canvas contains a reactive variable activeDocument
which tracks the document currently being rendered. Updating this property will render a new document.
// Create a Silk document:var myDoc = { title : newReactive("[untitled]")
, uri : newReactive("/page/untitled-document-31")
, node : $("<article />")
, dirty : newReactive(false)
}// Render the document on the canvas by// setting the 'activeDocument':Silk.canvas.activeDocument.set(myDoc);
Part of the responsibility of the canvas is to keep the text of the page’s first <h1>
element in sync with active document’s title. We can model this as a reactive connection that we set up when constructing the canvas. The value will be kept in sync during the entire lifetime of the element.
Because both the active document and the title property are reactive, we need bind
to project the title out.
// The Canvas constructor:functionCanvas (initialDoc)
{this.activeDocument = newReactive(initialDoc);this.node = $("#canvas");// Find the header and reactively set the text to// the active document's title:var title = this.activeDocument.bind(d => d.title);$("h1").first().rText(title);
}
The bind
method proves to be very useful here. When we now change the active document’s title, the header and canvas elements will stay up to date automatically. Interestingly, when we set a new active document, the elements will also change and now track the new document. We install the data bindings only once, during canvas construction. Still, all changes from now on will be reflected without any additional code!
So, why is bind
called bind
?
Again, the function name finds its origin in Haskell. Bind is one of the primitive methods from Haskell’s Monad
type class. Reactive variables form a monad, which means we can flatten nested layers of reactivity into one.
The correspondence between our JavaScript functions and the Haskell counterparts is shown below. Note that join
after map
is equivalent to bind
.
// JavaScript: -- Haskell:a.join() join aa.bind(f) a >>= fa.bind(f) do v <- a
g va.map(f).join() join (fmap a f)
We find return
(and pure
) as the constructor for reactive variables:
// JavaScript: -- Haskell:newReactive(a) return a
Additional Reactive Combinators
Besides the basic composition operators (lift
, map
, bind
, and join
), we have some other combinators that make working with reactive values worthwhile. To give you an idea what kind of combinators are possible with a reactive programming library like ours, we list some interesting functions with an example or their usages.
wait
: Wait for a condition on a reactive value to become true, run an action once and unlink immediately after.// Alert once when the user signs out:user.map(authenticated) .wait( (isAuth) => !isAuth // predicate , () => alert("no longer logged in!") // action );
sample
: A small combinator to sample changes over some period of time.// Send a new query string to the server if the string// representation of our internal data model changes, but// at most once every 5 seconds:queryData.map(computeQueryString) .sample(5000) .onchange(performQueryXHR);
collapse
: A bit likesample
, but runs once at the end of a JavaScript thread.// Render the username as the text contents of a <div>,// but render at most once per thread:$("div.username").rText(user.bind((u) => u.name).collapse());
bimap
: A bit likemap
, but connects two values in both directions. We specify two functions, one for every direction. The invariant is that both functions need to form a proper isomorphism (or at least converge to one).// Makes sure the 'halved' and 'doubled' variables// always keep there invariant:bimap( halved , doubled , (a) => a * 2 , (a) => Math.round(a / 2) );
Like you see here, those reactive combinators allow for a syntactically lightweight form of asynchronous programming.
Drawbacks
Even with a few very simple JavaScript types and combinators we are able to build very powerful data abstractions. The Silk client uses this reactive paradigm a lot and we can safely say this benefits the quality of our code base a lot. Unfortunately there are also several drawbacks to using FRP like this:
Resource management can be tricky. Every variable you link adds a node to the reactive graph, when the link isn’t need anymore you need to clean it up again. This requires some form of manual garbage collection. Cleaning up automatically is hard, because two linked values have a mutual references and JavaScript doesn’t have pointer finializers. We managed to build a system that can at least clean up intermediate unused parts of reactive networks, so minimal work of the library user is required. Also, it must be noted that cleaning up resources is equally hard in the case of normal event based systems, when using jQuery
on
andoff
for example.Occasionally care is needed to prevent value oscillation. When two values depend on eachother (with either a direct, or indirect cycle) values must converge, otherwise the network will echo value changes continuously. Non-converging networks isn’t a widely occurring problem, but when it does happen debugging can be painful.
Debugging can be surprising. Reactive networks a very good at propagating data changes through every part of your application. In that sense they work a bit like events. Debugging problems with reactive values can be tricky because problems in one part of your application might be caused by seemingly unrelated pieces of code. This needs some getting used to. Like most problems in JavaScript, a static type system could greatly benefit us here. We’re are currently investigating using TypeScript for parts of our code base to improve reasoning about our connections between different parts of the product.
Conclusion
Overall we’re very happy with our framework and would encourage other client side programmers to the explore the realm of FRP as well. Especially when building complicated user interfaces that need to keep up to date with frequent changes in the underlying data model, reactive data bindings can keep the amount of manual work down.
Discuss on: reddit or hacker news