reactive-function
A library for managing reactive data flows.
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());
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));
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.
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
Data flow graphs can be serialized to JSON, then visualized using graph-diagrams.
# ReactiveFunction.serializeGraph()
Returns a serialized form of the graph. Node names are derived from property.propertyName
for each property. If propertyName
is not specified, then the automaticelly generated node id (an integer) is used as the node name.
Example:
var firstName = ReactiveProperty("Jane");
var lastName = ReactiveProperty("Smith");
var fullName = ReactiveProperty();
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": "fullName" },
{ "id": "firstName" },
{ "id": "lastName" }
],
"links": [
{ "source": "firstName", "target": "fullName" },
{ "source": "lastName", "target": "fullName" }
]
}
See also graph.serialize().
Related Work