DerivableJS and Reactjs

Googling on React and state somehow I came over a video about Derivable. It is self sufficient library written by David Sheldrick that amplifies types in Javascript into a derivable. Primitives and objects can be amplified into a derivable called an atom which is mutable. Atoms can be composed into a non-mutable called a derivation. Through composition the state of a derivable is consistent with the state it derives from. Changes to a derivable may be bound to reactors.
How does Derivable work? The library wraps any JavaScript type and gives it special powers. We can elevate a JavaScript type with a couple different constructors (i.e. unit) and chain operations on them (i.e. bind). Atoms can encapsulate any JavaScript type. Below we chain together two primitives as atoms into a derivation:
import {atom, derivation} from 'derivable';
...let num = atom(5);
let str = atom('cats');
let combo = derivation(() => {
return num.get() + ' ' + str.get();
});// no graph until combo.get()
Every derivable has a get, derive, and reactor method. The atom constructor swallows the type and defines an internal state plus initializes DAG (directed acyclic graph) references. The code above does not result in a hidden graph between the derivation and the atoms it uses. Not yet. First the get method on the derivation has to be called thereby forcing the construction of the graph. A get for both atoms and derivations dumps the current derivable into the last array of arrays. The 2 dimensional array grows in proportion to the length of the DAG. The get for a derivation is where the 2 dimensional arrays and DAG grows.
When an atom is constructed it has a stable state. The newly constructed derivation has a new state plus a frozen value (i.e. an unchangeable Object with an equals method that always returns false). Pulling data (i.e. get) on a fresh derivation calculates it’s value and the internal DAG. Data can also be pushed into sinks or reactors. However, these sinks are also inactive regardless of a change to an ancestor until at least one reactor is forced to start. A forced reactor effectively calls get.
// continuing on from the code snippet above...
let reactor = combo.reactor(v => { // do stuff });
// no effect on combo or the reacting operation
num.set(2);
// but we can force a value and the graph with
reactor.force();
Fine. Pulling even indirectly through a forced reactor sets up a DAG from the perspective of the descendents. There are a bunch of cool shortcuts with the derive method that under the covers issues a derivation. A big atom can be picked apart with ease:
let big = atom({
name: 'Gary',
likes: ['fishing', 'episodes of Lost'],
married: false
});let [name, married] = big.derive(['name', 'married']);
// name.get() === 'Gary'
There are lenses for atoms. Sheldrick has a nice helper method for atoms called swap. This a glorified set that unpacks the underlying value and calls an operation with the value plus extra parameters:
let num = atom(5);
num.swap((v, factor) => v * factor, 2);
// num.get() === 10
Swap is the segway into mutating. Accessing data is fairly straightforward. What happens when data mutated? Remember this can only be done on atoms, not derivations. Sheldrick uses a nifty garbage collection process. When an atom is set with a new value the data is conditionally validated (if the atom has been cloned by withValidator), checked for equality and if not equal then the mark-react-sweep logic is kicked off. If a mutation isn’t inside a transaction then this is the flow:
- overwrite the internal value and set state as changed
- recursively mark descedents (except reactors because these are what is being collected) with the unstable state (if not already) and collecting reactors along the graph
- process the collection of reactors that pulls the DAG via get on ancestors just prior to executing a reactor
- sweep all nodes (i.e. atoms, derivations and reactors) cleaning up state
Grouping updates to one or more atoms inside a transaction just does the mark step deferring the reactor processing and sweeping until a commit. The following block defines a chain of reactors:
let num = atom(5);
let str = atom('cats');
let combo = derivation(() => num.get() + ' ' + str.get());
let upper = combo.derive(v => v.toUpperCase());
var last = 'nobody';
let rOne = combo.react(v => {last='one';}, {skipFirst: true});
let rTwo = upper.react(v => {last='two';}, {skipFirst: true});
num.set(2);
// last is going to be 'two'
The internal array of reactors has two members. The first is the second reactor rTwo and it’s value is already unstable as 5 CATS. The second is the first reactor rOne with a lower case cats. Processing goes through the array of reactors in reverse checking if each is active. Reactors can be started (i.e. active) and stopped (i.e. not active). Then if there is parent reactor it is processed after the current reactor is marked as yielding (which helps catch cyclical dependencies). rOne has a parent reactor. The react method really composes a series of reactors as timers with conditions like skipFirst passed as an argument. Once, parent reactors are resolved if the parent node is unstable, new, orphaned or disowned it’s get method is called. The parent’s state is unchanged then current reactor isn’t executed. But a parent state of changed fires the current reactor.
Sweeping begins at the mutated atom. If it’s state is changed or unchanged then sweeping continues on recursively to the node’s children and finally marks the atom as stable.
The simple answer to how DerivableJS work is that access (i.e. get) on a atom grabs the value leaving the state as stable. Access on a derivation is through a DAG calculating the value bottom to top leaving the state as either changed or unchanged. Access is either explicit with get or indirect when a reactor is forced to start. Mutation (i.e. set) is only on atoms. It marks top to bottom nodes (atoms, derivations and reactors) as unstable and then collects reactors for execution. When reactors are processed nodes that are unstable should because changed or unchanged which are later cleaned up as stable by sweeping.
We did a rewrite of the Todo MVC example that ships with DerivableJS. Sheldrick has provided great examples and test cases. We just wanted the Todo code to resemble how we structure our React applications. The code snippets above exist also as Mocha tests in the More.Derivable.js file under the test directory.
DerivableJS is a compact and well written library that parallels Redux, Mobx and other state monitors. The stores for a React application can have a few atoms. A time report application would have an atom for the signed user object, an atom for a map of activities and an atom for a map of time cards to stamp. Then for the different React components derivations on these atoms could be bundled with a single reactor which drives stateless React functional components (i.e. similar to the Todo MVC example). That setup is ease to test, read and understand.