Values
The root can hold nodes; now we make some. The two simplest are a constant, a value that never changes, and a piece of state, a value which can be changed and the program's output will be updated accordingly.
const bestYear = Reactive.Constant(1990);
const score = Reactive.State(0);
The Node
A node is an entry inside the current scope. It carries a tag that marks it as a reactive node, the scope it lives in, a payload that tags what kind of node it is, and its latest value.
{
[Reactive._reactive]: true, // marks this object as a reactive node
scope, // the scope this node lives in
payload: { constant: {} }, // what kind of node this is
value: 1990, // its latest value
};
Keeping the payload and the value apart is deliberate: the payload stays fixed while the value changes as the
program updates. Nodes are pushed onto the scope's nodes array in build order.
Reactive References
Calling Reactive.Constant or Reactive.State gives you back the node itself. A
reactive reference is just that node object, marked with the reactive tag:
{ [Reactive._reactive]: true, scope, payload: { constant: {} }, value: 1990 }
The reference is the node, so there is nothing to look up: the same object holds the latest value, which changes in place as the program updates. Everything downstream, a calculation, a piece of the page, holds these node references and reads through them.
Creating a Node
Both constructors share one builder. _nodeCreate finds the current scope, builds the node tagged as
reactive and carrying its scope, pushes it onto that scope, and returns the node, which is its own reference:
Reactive._nodeCreate = (payload, value) => {
const scope = Reactive._currentScope;
const node = { [Reactive._reactive]: true, scope, payload, value };
scope.nodes.push(node);
return node;
};
Reactive.Constant
A constant tags its payload, and hands its fixed value straight to _nodeCreate. It has no inputs and
never changes:
Reactive.Constant = (value) => {
const payload = { constant: {} };
return Reactive._nodeCreate(payload, value);
};
A constant payload just needs to identify itself as a constant; it carries no data of its own.
Reactive.State
State is the same shape, tagged state so later steps can tell it apart, and seeded through
_value so it starts from a plain snapshot of what you pass:
Reactive.State = (initialValue) => {
const payload = { state: {} };
const value = Reactive._value(initialValue);
return Reactive._nodeCreate(payload, value);
};
A state payload just needs to identify itself as state; like the constant, it carries no data of its own.
Helper Functions
A node is tagged with one shared Symbol, so a reactive reference is told apart from a plain object with no chance of a name clash:
Reactive._reactive = Symbol('reactive node');
A reactive reference is an object carrying that tag, so a small check tells one from a plain value:
Reactive._isReactive = (value) => {
return value !== null && typeof value === 'object' && value[Reactive._reactive] === true;
};
Seeding a State should copy a value, not a reference, so _value resolves a reference to the value
behind it and passes a plain value straight through. Because a reference is the node, resolving it just reads the
node's value. It grows to walk arrays and objects once we have them, so a value nested at any depth resolves
too:
Reactive._value = (value) => {
if (Reactive._isReactive(value)) {
return value.value;
}
return value;
};
Running It
A program can now put something in the root scope:
const root = Reactive.Root(() => {
const bestYear = Reactive.Constant(1990);
const score = Reactive.State(0);
});
const rootScope = root.rootScope;
rootScope.nodes[0]; // { payload: { constant: {} }, value: 1990, ... }
rootScope.nodes[1]; // { payload: { state: {} }, value: 0, ... }
rootScope.nodes.length; // 2 two nodes, entries in the root scope
Next Steps
Our program can now contain values, so let's introduce the ability to perform calculations on these values.