Context
To pass a value down through many layers by hand is tedious. Every call in between must carry it. Context lets an outer scope provide a value under a name. A scope nested inside it reads that value directly, without threading it through everything in between.
Reactive.Context('theme', theme); // provide it once, up high
// ...deep inside some nested block...
const theme = Reactive.Use('theme'); // read it, no threading in between
Providing a Value
We store the provision on the scope. A scope gains one more field beside its nodes and its owner. It is a
contexts object, a map of name to value, made on first use:
const scope = {
nodes: [],
parentNode,
currentEmissionsRef: undefined,
contexts: undefined, // name → value, made when the first Context is provided
};
Context writes into the current scope's map, so everything built after it in this scope, and in every
scope nested inside it, can read the name:
Reactive.Context = (name, value) => {
const scope = Reactive._currentScope;
if (scope.contexts === undefined) {
scope.contexts = {};
}
scope.contexts[name] = value;
};
Reading It
Use goes up the scope tree. It checks the current scope first, then the scope
its owner sits in, and so on to the root. It returns the first contexts that has the name. A nearer
provision shadows a further one. A name that no scope provides is a mistake. We catch it at build:
Reactive.Use = (name) => {
let scope = Reactive._currentScope;
while (scope !== undefined) {
if (scope.contexts !== undefined && Object.prototype.hasOwnProperty.call(scope.contexts, name)) {
return scope.contexts[name];
}
const owner = scope.parentNode; // the node that owns this scope
scope = owner === undefined ? undefined : owner.scope; // climb to the owner's scope
}
throw new Error('Reactive.Use: no context named "' + name + '"');
};
Because it goes up the live scope tree, a block raised later, an If body or an Each entry, still finds a context that an ancestor provided. Its owner leads back up to the scope that set it.
It Is Just a Value
Use returns exactly what you provided, usually a reactive value. So a calculation over it tracks it
like any other input. When you set the provided state, Reactive updates every reader, wherever they sit in the tree:
const label = Reactive.Calculate([Reactive.Use('theme')], (t) => 'theme: ' + t);
// label reflows whenever the provided theme state changes
Running It
The root provides a reactive theme and a plain level. Inside an
If body, Use('theme') finds the ancestor's theme. Use('level') sees the
outer 1, until a nearer Context shadows it with 2:
const root = Reactive.Root(() => {
const theme = Reactive.State('dark');
Reactive.Context('theme', theme);
Reactive.Context('level', 1);
Reactive.If(true, () => {
Reactive.Context('level', 2); // shadows the outer level
Reactive.Emit('themed', Reactive.Use('theme')); // 'dark', from the ancestor
Reactive.Use('level'); // 2, the nearer provision
});
Reactive.Use('level'); // 1, the outer provision
});
// later: setting the theme state updates everything that used it
Next Steps
Context threads a value down the scope tree, so a deep block reads what an ancestor provided without every layer carrying it. Next, If is the first change of shape. It is a block that builds in its own scope only when a condition holds.