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);
A constant that never changes, and a piece of state you can change later

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
};
A node: a tag, its scope, a tagged kind, and a 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 }
A reactive reference is the node object itself

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;
};
Push a node onto the current scope, and return the node itself

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 node with a fixed value and no inputs

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 node whose value can change later

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');
The tag that marks a 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;
};
Telling a reference from a plain value

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;
};
Resolving a reference to a plain 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
Two nodes, two values, 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.