Skip to content

datavis-tech/reactive-function

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

reactive-function

A library for managing reactive data flows.

NPM NPM Build Status

This library provides the ability to define reactive data flows by modeling application state as a directed graph and using topological sorting to compute the order in which changes should be propagated.


This library is built on top of reactive-property and graph-data-structure.

Table of Contents

Examples

Full Name

Suppose you have two reactive properties to represent someone's first and last name.

var firstName = ReactiveProperty("Jane");
var lastName = ReactiveProperty("Smith");

Another reactive property can represent the full name of the person.

var fullName = ReactiveProperty();

You could set the full name value like this.

fullName(firstName() + " " + lastName());

However, the above code sets the value of fullName only once. Here's how you can define a ReactiveFunction that automatically updates fullName whenever firstName or lastName change.

var reactiveFunction = ReactiveFunction({
  inputs: [firstName, lastName],
  output: fullName,
  callback: function (first, last){
    return first + " " + last;
  }
});


The data flow graph for the example code above.

Whenever firstName or lastName change, the callback defined above will be executed on the next animation frame. If you don't want to wait until the next animation frame, you can force a synchronous evaluation of the data flow graph by invoking digest.

ReactiveFunction.digest();

Now you can access the computed fullName value by invoking it as a getter.

console.log(fullName()); // Prints "Jane Smith"

ABC

The output of one reactive function can be used as an input to another.


Here, b is both an output and an input.

var a = ReactiveProperty(5);
var b = ReactiveProperty();
var c = ReactiveProperty();

ReactiveFunction({
  inputs: [a],
  output: b,
  callback: function (a){ return a * 2; }
});

ReactiveFunction({
  inputs: [b],
  output: c,
  callback: function (b){ return b / 2; }
});

ReactiveFunction.digest();
assert.equal(c(), 5);

Tricky Case

This is the case where Model.js fails because it uses Breadth-first Search to propagate changes. In this graph, propagation using breadth-first search would cause e to be set twice, and the first time it would be set with an inconsistent state. This fundamental flaw cropped up as flashes of inconstistent states in some interactive visualizations built on Model.js. For example, it happens when you change the X column in this Magic Heat Map. This flaw in Model.js is the main inspiration for making this library and using topological sort, which is the correct algorithm for propagating data flows and avoiding inconsistent states.


The tricky case, where breadth-first propagation fails.

var a = ReactiveProperty(5);
var b = ReactiveProperty();
var c = ReactiveProperty();
var d = ReactiveProperty();
var e = ReactiveProperty();

ReactiveFunction({ inputs: [a],    output: b, callback: function (a){    return a * 2; } });
ReactiveFunction({ inputs: [b],    output: c, callback: function (b){    return b + 5; } });
ReactiveFunction({ inputs: [a],    output: d, callback: function (a){    return a * 3; } });
ReactiveFunction({ inputs: [c, d], output: e, callback: function (c, d){ return c + d; } });

ReactiveFunction.digest();
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));

a(10);
ReactiveFunction.digest();
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));

Ohm's Law

Ohm's Law is a mathematical relationship between 3 quantities in electrical circuits:

  • V, voltage. V = IR
  • I, current. I = V/R
  • R, resistance. R = V/I

Given any two of these values, one can calculate the third. Here's an example where if any two of the values are set, the third will automatically be calculated.


The data flow graph for Ohm's Law.

var I = ReactiveProperty();
var V = ReactiveProperty();
var R = ReactiveProperty();

ReactiveFunction({ output: V, inputs: [I, R], callback: function (i, r){ return i * r; } });
ReactiveFunction({ output: I, inputs: [V, R], callback: function (v, r){ return v / r; } });
ReactiveFunction({ output: R, inputs: [V, I], callback: function (v, i){ return v / i; } });

V(9)
I(2)
ReactiveFunction.digest();
console.log(R()); // Prints 4.5

R(6)
I(2)
ReactiveFunction.digest();
console.log(V()); // Prints 12

V(9);
R(18);
ReactiveFunction.digest();
console.log(I()); // Prints 0.5

For more detailed example code, have a look at the tests.

Installing

If you are using NPM, install this package with:

npm install reactive-function

Require it in your code like this:

var ReactiveFunction = require("reactive-function");

This library is designed to work with reactive-property, you'll need that too.

npm install reactive-property

var ReactiveProperty = require("reactive-property");

API Reference

Managing Reactive Functions

# ReactiveFunction(options)

Construct a new reactive function. The options argument should have the following properties.

  • inputs - The input properties. An array of ReactiveProperty instances.
  • output (optional) - The output property. An instance of ReactiveProperty.
  • callback - The reactive function callback. Arguments are values of inputs. The return value will be assigned to output.

This constructor sets up a reactive function such that callback be invoked

  • when all input properties are defined,
  • after any input properties change,
  • during a digest.

An input property is considered "defined" if it has any value other than undefined. The special value null is considered to be defined.

An input property is considered "changed" when

  • the reactive function is initially set up, and
  • whenever its value is set.

Input properties for one reactive function may also be outputs of another.

# reactiveFunction.destroy()

Cleans up resources allocated to this reactive function.

More specifically:

  • Removes listeners from inputs.
  • Removes edges from the data flow graph (from each input).
  • Removes property nodes from the data flow graph if they have no incoming or outgoing edges.

You should invoke this function when finished using reactive functions in order to avoid memory leaks.

# ReactiveFunction.link(propertyA, propertyB)

Sets up one-way data binding from propertyA to propertyB. Returns an instance of ReactiveFunction.

Arguments:

Example:

var a = ReactiveProperty(5);
var b = ReactiveProperty(10);
var link = ReactiveFunction.link(a, b);

This is equivalent to:

var a = ReactiveProperty(5);
var b = ReactiveProperty(10);
var link = ReactiveFunction({
  inputs: [a],
  output: b,
  callback: function (a){ return a; }
});

Data Flow Execution

# ReactiveFunction.digest()

Propagates changes from input properties through the data flow graph defined by all reactive properties using topological sorting. An edge in the data flow graph corresponds to a case where the output of one reactive function is used as an input to another.

Whenever any input properties for any reactive function change, digest is debounced (scheduled for invocation) on the nextFrame. Because it is debounced, multiple synchronous changes to input properties collapse into a single digest invocation.

Digests are debounced to the next animation frame rather than the next tick because browsers will render the page at most every animation frame (approximately 60 frames per second). This means that if DOM manipulations are triggered by reactive functions, and input properties are changed more frequently than 60 times per second (e.g. mouse or keyboard events), the DOM manipulations will only occur at most 60 times per second, not more than that.

# ReactiveFunction.nextFrame(callback)

Schedules the given function to execute on the next animation frame or next tick.

This is a simple polyfill for requestAnimationFrame that falls back to setTimeout. The main reason for having this is for use in the tests, which run in a Node.js environment where requestAnimationFrame is not available. Automatic digests are debounced against this function.

Serialization

# ReactiveFunction.serializeGraph()

Serializes the data flow graph. Returns an object with the following properties.

  • nodes An array of objects, each with the following properties.
    • id The node identifier string.
    • propertyName The property name for this node, derived from property.propertyName for each property.
  • links An array of objects representing edges, each with the following properties.
    • source The node identifier string of the source node (u).
    • target The node identifier string of the target node (v).

Example:

var firstName = ReactiveProperty("Jane");
var lastName = ReactiveProperty("Smith");
var fullName = ReactiveProperty();

// For serialization.
firstName.propertyName = "firstName";
lastName.propertyName = "lastName";
fullName.propertyName = "fullName";

ReactiveFunction({
  inputs: [firstName, lastName],
  output: fullName,
  callback: function (first, last){
    return first + " " + last;
  }
});

var serialized = ReactiveFunction.serializeGraph();

The value of serialized will be:

{
  "nodes": [
    { "id": "95", "propertyName": "fullName" },
    { "id": "96", "propertyName": "firstName" },
    { "id": "97", "propertyName": "lastName" }
  ],
  "links": [
    { "source": "96", "target": "95" },
    { "source": "97", "target": "95" }
  ]
}

See also:

Related Work