Functional reactive programming library
Reactor is a lightweight library for reactive programming. It provides reactive variables which automatically update themselves when the things they depend on change. This is similar to how a spreadsheet works, where cells can automatically change their own values in reaction to changes in other cells.
Here's a quick example of what Reactor does:
var foo = Signal1;var bar = Signalreturn foo + 1;;foo; // 1bar; // 2foo6;foo; // 6bar; // 7
You declare how a variable should be calculated once, and it automatically recalculates itself when necessary. This makes it easy to keep a complex data model consistent, and a user interface up to date when a model is changed.
Reactor is designed to be unobtrusive and unopinionated.
- There is no need to manually declare listeners or bindings. Reactor automatically keeps track of all that for you.
- It imposes no particular structure on your code. Any variable can be easily replaced with a reactive one.
- There is no need to learn special special syntax or use special classes, objects, or methods.
Reactor has just 2 components: signals and observers
- Signals are values that are expected to change over time. Signals can depend on each other.
- Observers are functions which are triggered on signal changes.
A signal depends on another signal when it requires the other signal's value to determine its own. Similarly, an observer depends on a signal when it requires that signal's value to determine what to do.
Whenever a signal is updated, it automatically updates all its dependent signals & observers as well. Together, signals and observers form a graph representing the propagation of data throughout the application. Signals sit on the inside of the graph representing the internal data model, while observers are on the edges of the graph affecting external systems.
Here is an example of simple note list application using Reactor. A signal is used to represent user data while an observer is used to update the html.
// The model is just an array of strings wrapped in a SignalnoteList = Signal"sample note" "another sample note";// The code to display the notes is wrapped in an Observer// This code is automatically retriggered when the noteList is modifiedObservervar noteListElement = documentgetElementById"noteList";noteListElementinnerHTML = '';// "noteList().length" causes a read of noteList// This automatically sets noteList as a dependency of this Observer// When noteList is updated it automatically retriggers the whole code blockfor var i = 0; i < noteListlength; i++var noteElement = documentcreateElement"div";noteElementtextContent = noteListi;noteListElementappendChildnoteElement;;// The input only needs to modify the Signal// The UI update is automatically handled by the Observervar noteInputElement = documentgetElementById"noteInput";if eventkeyCode === 13noteListpushnoteInputElementvalue;noteInputElementvalue = '';;
Reactor adds very little overhead. You use
Observer to wrap the appropriate variables and code blocks, swap reads and assignments for function calls, and you're good to go!
Reactor is based on the same reactive principles as Bacon.js and Knockout.js. The main difference is that Reactor is trying to be lightweight and keep the additional syntax and complexity to a minimum. Reactor sets dependencies for you automatically so there is no need to manually set subscriptions/listeners.
Compared to Knockout, Reactor does not provide semantic bindings directly to HTML. Instead, users set the appropriate HTML modifying functions as Observers.
Compared to Bacon, Reactor does not help to handle event streams.
Signal are just values which other signals can depend on.
Reactor provides a global function called
Signal. It wraps a given value and returns it as a signal object.
var foo = Signal7;
Signal objects are implemented as functions. To read the value of a signal, call it without any arguments.
foo; // returns 7
To change the value of a signal, pass it the new value as an argument.
foo9; // sets the signal's value to 9
Signals can take on any value: numbers, strings, booleans, and even arrays and objects.
foo2.39232;foo"cheese cakes";footrue;foo"x" "y" "z";foo"moo": bar;
However, if a signal is given a function, it takes the return value of the function as its value instead of the function itself.
var foo = Signalreturn 2 * 3;;foo; // returns 6 instead of the function
Signals can have their value depend on other signals by using functions. If a different signal is read from within the given function, then that signal will automatically be set as a dependency. This means that when the dependency has been updated, the value of the dependent signal will be updated as well.
var foo = Signal7;var bar = Signalreturn foo * foo; // since foo is read here,// is is registered as a dependency of bar;foo; // returns 7 as expectedbar; // returns 49 since it is defined as foo() * foo()foo10; // this updates the value of foo// and the value of bar as wellbar; // returns 100 since it was automatically updated together with foo
Notice that there is no need to declare any listeners or bindings. Reactor automatically finds these dependencies in a signal's function definition.
This automatic updating allows signals to be linked together to form more complex dependency graphs.
var firstName = Signal"Gob";var lastName = Signal"Bluth";// fullName depends on both firstName and lastNamevar fullName = Signalreturn firstName + " " + lastName;;// barbarianName depends only on firstNamevar barbarianName = Signalreturn firstName + " the Chicken";// comicTitle depends on barbrianName and fullName and therefore// indirectly depending on firstName and lastNamevar comicTitle = Signalreturn "He who was once " + fullName + " is now " + barbarianName;;firstName; // "Gob"lastName; // "Bluth"fullName; // "Gob Bluth"barbarianName; // "Gob the Chicken"comicTitle; // "He who was once Gob Bluth is now Gob the Chicken"firstName"Michael"; // updating firstname automatically updates// fullName, barbarianName, and comicTitlefirstName; // "Michael"lastName; // "Bluth"fullName; // "Michael Bluth"barbarianName; // "Michael the Chicken"comicTitle; // "He who was once Michael Bluth is now Michael the Chicken"
As far as possible, signals should only read and avoid having any external effects. This refers to actions such as:
- Modifying the HTML
- Writing to disk
- Logging an interaction
- Triggering an alert
In a complex graph, a changed valued might cascade and cause some dependent signals' definitions to be invoked multiple times before propagation is complete.
In the example above, updating
firstName first causes both
barbarianName to update. This causes
comicTitle to be updated twice. Once when
fullName is updated, and again when
barbarianName is updated. This is fine on its own since there are no external effects.
However, if the definition of
comicTitle included writing to disk then it would cause problems. Because
comicTitle is updated twice, it would initiate 2 different writes. Furthermore, the first write would be incorrect as the change propagation would not have completed yet.
For external effects, it is recommended that observers are used instead.
Observers are almost identical to signals except for 3 main differences:
- They are triggered only after all signals have been updated
- They are only triggered once per signal update
- Unlike signals, observers cannot be depended upon
Observers are used for external effects while signals are used for internal state. Signal functions might trigger multiple times before all signals have finished updating. If signals are used for external effects, they could triggered incorrectly and redundantly. Observers are triggered last and only once per update and therefore do not have this problem.
Observers are created in the same way as signals.
var foo = Signal"random string";var bar = Observer // Triggers first on creationalertfoo; // Reads from foo and alerts "random string"; // Is automatically registered as a dependent// and triggers again whenever foo is changed
Just like signals, their dependencies are automatically calculated and triggered when the appropriate signal is updated.
foo"a new random string"; // triggers bar which// alerts "a new random string"
Just like signals, their functions can be updated.
// change bar update the html instead of alerting// triggers once immediately after updatingbarfooElement = documentgetElementById"foo";fooElementtextContent = foo;;foo"this string will be logged now"; // triggers bar which now// logs the string instead
To disable an observer, pass in a null value.
barnull; // disables the observer
When updating Arrays and Objects, you should use Reactor's convenience methods instead of updating the objects directly. This means you should use:
foo.set(key, value)instead of
foo()[key] = value
foo.splice(start, length)instead of
The reason for this is if a signal has an array as its value, directly updating the array will not update the signal's dependants. Because the signal object is still representing the same array, it does not detect the change. Instead, using the provided convenience methods does the same update but allows the change to be detected. This applies to objects as well.
// foo initialized as a signal with an array as its valuevar foo = Signal"a" "b" "c";// bar initialized as a signal whos value depends on foovar bar = Signalreturn foojoin"-";;foo; // ["a","b","c"]bar; // "a-b-c"// Updating foo's array directly does not trigger an update of barfoopush"d";foo; // ["a","b","c","d"]bar; // "a-b-c"// Instead, updating using the convenience method does trigger the update of barfoopush"e";foo; // ["a","b","c","d","e"]bar; // "a-b-c-d-e"
var stringSignal = Signal"a string"; // Signals can be set to any valuevar booleanSignal = Signaltrue;var numberSignal = Signal1;var dependentSignal = Signal // If given a function, the signal's value// is the return value of the function// instead of the function itselfreturn numberSignal + 1; // Reading from another signal automatically sets it// as a dependency;var stringSignal"a new string value"; // To update a signal just pass it a new value// this automatically updates all// its dependents as wellvar arraySignal = Signal // Signals can even be arrays or objectsstringSignal // which contain other signalsbooleanSignalnumberSignal;var alertObserver = Observer // Observers are just like signals except:alertarraySignaljoin","; // They are updated last; // They are only updated once per propagation// They cannot be depended on by signalsarraySignalset4 "a new value!" // Convenience method for setting properties// on an Array or Object SignalarraySignalpush"foo"; // Convenience methods for updating an array SignalarraySignalpop;arraySignalunshift"bar";arraySignalshift;arraySignalreverse;arraySignalsort;arraySignalsplice1 2 "not a signal";
And if you like Coffeescript, Reactor gets even simpler!
stringSignal = Signal "a string" # Signals can be set to any valuebooleanSignal = Signal truenumberSignal = Signal 1dependentSignal = Signal -> numberSignal + 1 # If given a function, the signal's value# is the return value of the function# instead of the function itself# Reading from another signal automatically# sets it as a dependencystringSignal "a new string value" # To update a signal just pass it a new value# this automatically updates all# its dependents as wellarraySignal = Signal # Signals can even be arrays or objectsstringSignal # which contain other signalsbooleanSignalnumberSignalalertObserver = Observer -> # Observers are just like signals except:alert arraySignaljoin"," # They are updated last# They are only updated once per propagation# They cannot be depended on by signalsarraySignalset 4"a new value!" # Convenience method for setting properties# on an object SignalarraySignalpush "foo" # Convenience methods for updating an array SignalarraySignalpoparraySignalunshift"bar"arraySignalshiftarraySignalreversearraySignalsortarraySignalsplice 12"not a signal"
Download reactor.js and include it in your application.
Reactor has just 2 components:
- For browsers, they will be bound to window as global objects for use anywhere.
- For node.js, they will be bound as properties of the exports object to be imported as modules
$ npm install reactorjs
And importing it into your application by adding
Reactor = require"reactorjs";Signal = ReactorSignal;Observer = ReactorObserver;
For the lucky people using both coffeescript and node.js: you can just use