To create a pointer for any JavaScript value, you can use the $$
helper function.
The always
helper function lets you define a reactive pointer that depends on other
pointers.
const refA = $$(5);
const refB = $$(0);
const refSum = always(() => refA + refB);
refB.val = 5;
console.log(refSum.val) // 10
This creates the pointers refA
, refB
and a reactive transformed
refSum
pointer that gets updated every time refA
or refB
are changed.
Pointer can also be created for non-primitive values (JSON Objects, Maps, Sets, ...).
const refObj = $$({
x: 100,
y: 200
});
const refArray1: string[] = $$(['some', 'example', 'values']);
The created reference object can be used like a normal object. You can access all properties and methods.
refObj.x; // 100
refObj.x = 50;
refArray[0] // 'some'
refArray.push('more');
The underlying references for the properties of an object can be accessed via the special $
property:
refObj.$.x // Datex.Ref<number>
With the $$
property, a strong reference to the property (pointer property) can be created.
A pointer property will always point to the reference assigned to the property name.
const propX = refObj.$$.x; // Datex.PointerProperty<number>
refObj.x = 10; // update the value of refObj.x
propX.val // 10, same as with normal reference
refObj.$.x = $$(4); // assign a new reference to refObj.x
propX.val // 4, points to the newly assigned reference
Alternatively, the $$
function can be used:
$$(refObj, "x") // Datex.PointerProperty<number>
In most cases, when an object is bound to a pointer, its property values are automatically bound to pointers recursively:
const map = new Map([
['y', {
a: 10,
b: 20
}]
]);
Datex.Ref.isRef(map) // false, not bound to a pointer
Datex.Ref.isRef(map.get('y')) // false, not bound to a pointer
const nestedObject = $$({
map: map
})
nestedObject.map // Map
Datex.Ref.isRef(map) // true, was implicitly bound to a pointer
Datex.Ref.isRef(map.get('y')) // true, was implicitly bound to a pointer
There are some exceptions to this behaviour:
- Primitive property values are not converted to pointers per default
- Normal class instances (
js:Object
) are not converted to pointers per default. Instances ofstruct
classes are still converted to pointers - When a class instances is directly bound to a pointer with
$$()
, its properties are not converted to pointers per default (like 2., this does not affectstruct
class instances
With DATEX, primitive values can also be used as references.
Since JavaScript does not support references for primitive values (e.g. numbers, strings, booleans),
primitive references are always wrapped in a Datex.Pointer
object to keep the reference intact:
const refA: Datex.Pointer<number> = $$(5);
The advantage of having the Datex.Pointer
interface always exposed as a primitive value wrapper is that utility methods like observe
can be easily accessed:
refA.observe(a => console.log(`refA was updated: ${a}`)); // called every time the value of refA is changed
Primitive pointers are still automatically converted to their primitive representation in some contexts, but keep in mind that the references are lost at this point:
const refX = $$(2);
const refY = $$(3);
const result = (refX * refY) + 6; // = 12 (a normal JS primitive value)
Warning
In certain cases, it is required to use the .val
property because the type coercion does not behave as you might expect.
Primitive pointers can be compared with a weak equality operator (==
), but we do not encourage this,
because type coercion of the weak equality operator can lead to unexpected results.
To compare pointer values, always compare their .val
properties with a strict equality operator:
const refString1 = $$("hello");
const refString2 = $$("hello");
console.log(refString1.val === refString2.val); // true
console.log(refString1 === refString2); // false, not the same reference
A similar problem occurs when using boolean operators like !
on a non-collapsed boolean pointer:
const refBool = $$(false);
if (!refBool) console.log("bool is false") // expected branch to be executed
else console.log("bool is true") // actually executed
When using boolean operators, always compare their .val
properties.
Consider using dedicated transform functions for boolean or comparison transforms.
The always()
function automatically determines all dependency values and recalculates when one of the dependencies changes.
For this reason, this function is very flexible and can be used for simple calculations or more complex functions.
The always()
function is just one of a group of so-called Transform functions.
There exist multiple transform functions that are optimizied for specific use cases like mathematical calculations
and can be used instead of a generic always()
function.
Read more about transform functions in the chapter Functional Programming.
With transform functions, values can be defined declaratively.
Still, there are some scenarios where the actual pointer value change event must be handled with custom logic.
For this scenario, the effect()
function can be used.
On the first glance, the effect()
function works similarly to the always()
function:
It is called every time a dependency value changes,
but in contrast to the always()
function, it does not create a new pointer.
An effect()
handler can also have side-effects (hence the name).
const id = $$(42);
// define a new effect (is immediately invoked)
effect(() => {
console.log("new id:" + id);
fetch(`https://api.example.com?id=${id}`).then(...)
})
id.val = 35; // triggers the fetch effect again
Warning
Keep in mind that effect handler are only triggered by pointer updates. Updating the value of a plain JavaScript variable does not work:
let id = 10;
effect(() => console.log("id: " + id));
id = 12; // does not trigger the effect
The effect()
function returns an object with a dispose()
method that can be called to clear the effect.
function task() {
const x = $$(0);
// effect is run every time x changes
const {dispose} = effect(() => console.log("x = " + x));
for await (x of y) {
x.val++;
}
// dispose effect
dispose();
}
Alternatively, effects can be restricted to the lifetime of a scope with the using
keyword.
function task() {
const x = $$(0);
// effect is run every time x changes
using e1 = effect(() => console.log("x = " + x));
for await (x of y) {
x.val++;
}
// effect is automatically disposed at the end of this scope
}
Effects can also be automatically disposed by using weak value bindings. The effect is only active as long as none of the weakly bound values is garbage collected:
const x = $$(42)
// bind x to the effect as a weak value
effect(
// effect function, weak variables are passed in via function arguments
({x}) => {
console.log("x is " + x);
},
// list of weak variable bindings:
{x}
)
As soon the the weakly bound value (x
in the example above) is no longer referenced anywhere,
it is garbage colleted and the effect is removed.
Weak value bindings can be used with all object values, not just with pointers.
Effect callbacks cannot be async
functions.
If you need to handle async operations, you can instead call an async function from inside the
effect callback:
const searchName = $$("");
const searchAge = $$(18);
// async function that searches for a user and shows the result somewhere
async function searchUser(name: string, age: number) {
const user = await fetchUserFromServer({name, age});
showUser(user);
}
// effect that triggers the user search every time searchName or searchAge is changed
effect(() => searchUser(searchName.val, searchAge.val))
All dependency values of the effect must be accessed synchronously.
This means that the variables inside the async function don't trigger the effect, only the ones passed
into the searchUser
call.
Due to the nature of JavaScript, synchronous effects are always executed sequentially. But often, effects need to be asynchronous, e.g. to fetch some data from the network.
Per default, if an effect is triggered multiple times in quick succession (e.g. due to a user input in a search field), the fetch requests are executed in parallel, but it cannot be guaranteed that the fetch for the last triggered effect is also resolved last. This leads to indeterministic behaviour.
To prevent this, you can force sequential execution of effects by returning a Promise
from the
effect handler function.
With this, it is guaranteed that the effect will not be triggered again before the last Promise
has resolved.
In the example above, sequential execution is enabled because the Promise
returned by the searchValue()
call is returned from the effect handler.
You can achieve parallel effect execution by not returning the Promise
, e.g.:
effect(() => {searchUser(searchName.val, searchAge.val)})
Note
With sequential async execution, it is not guaranteed that the effect is triggered for each state change - some states might be skipped. However, it is always guaranteed that the effect is triggered for the latest state at some point in time.
For more fine grained control, the observe()
function can be used to handle pointer value updates.
In contrast to effect()
, the observe()
function does not automatically determine dependency values -
they are explicitly specified.
const ptr = $$(10);
// log on value change
observe(ptr, value => console.log(`ptr value is now ${value}`));
// equivalent: instance method for primitive pointers
ptr.observe(nr, value => console.log(`ptr value is now ${value}`));
ptr.val++; // logs "ptr value is now 11"
An observer callback function gets called with up to 5 arguments:
(
value: any, // the new value
key?: any, // if a property of the pointer was changed, key contains the property key
// and value contains the property value
type: Ref.UPDATE_TYPE, // update type that triggered the observer
isTransform?: boolean, // true if the observer was triggered by a transform function
isChildUpdate?: boolean // true if the observer was triggered recursively by a child update
) => {
// ...
}
The following update types exist:
enum Ref.UPDATE_TYPE {
INIT, // pointer value was set for the first time
UPDATE, // pointer value was updated
SET, // a property was set
DELETE, // a property was deleted
CLEAR, // the value was cleared (all properties removed)
ADD, // a child value was added (e.g. for Sets)
REMOVE, // a child value was removed
BEFORE_DELETE, // called before DELETE, before the property gets deleted from the value
BEFORE_REMOVE // called before REMOVE, before the property gets removed from the value
}
Calling unobserve()
with the same callback function that was passed to observe()
removes the observer.
const ptr = $$(10);
const observer = value => console.log(`ptr value is now ${value}`);
// enable observer
observe(ptr, observer);
nr.val++; // logs "nr value is now 11"
// disable observer
unobserve(ptr, observer);
nr.val++; // observer not triggered
Non-primitive pointer values are normally always passed in their collapsed form (normal JavaScript object representation).
In contrast, primitive pointer values and pointer properties are always passed as Datex.Ref
values and have to be collapsed to get the normal JavaScript represententation (e.g. Datex.Ref<number>
-> number
).
For this purpose, the val()
helper
function can be used:
const refX: Datex.Ref<number> = $$(42);
const valX: number = val(refX);
If a non-reference value (e.g. a normal number
or object) is passed to the val
function, the value is just returned, so that it is guaranteed to always return a normal JavaScript value.