Reputation: 7151
Tracker computations are not re-run when a native variable change:
var foo = 'foo';
Tracker.autorun(function logFoo() { console.log('foo is:', foo); });
This code will be executed only one time:
foo is:
'foo'
The computation has no dependency, no _onInvalidateCallback
. It's pretty much dead.
There is however a lot of cases where I do need a native JavaScript variable or an object field to somehow run reactively inside Tracker computations (native API not fully ported to Meteor, ...)
Of course I can not simply write:
foo = new ReactiveVar(foo);
Since I will break the reference for the current frame and other functions may use another reference for foo
, thus desynchronisation and pain and headaches.
In a similar way...
obj.foo = new ReactiveVar(obj.foo);
This will also break, since obj.foo
is now completely different and code depending on obj.foo
being a simple, non-reactive value will immediately break.
It is also of no use against a module pattern (an isolated reference to obj.foo
) and will cause more desynchronisation and more pain and even more headaches.
How can I properly change a native Javascript variable or object field to a Reactive-Var without breaking legacy code?
Upvotes: 5
Views: 2691
Reputation: 7151
The two cases, native variable and object field, need to be taken separately, they will require different approaches. The first one will use a simple yet dirty trick, the second a more advanced trick.
Let's start with the native variable case.
If the variable is a writable object field, then we can change the reference to make a custom get/set couple linked to a reactive variable in a closure.
It's as simple as that:
function reactivise(obj, field) {
var rvar = new ReactiveVar(obj[field]);
Object.defineProperty(obj, field, {
get : function() {
return rvar.get();
},
set : function(value) {
rvar.set(value);
return value;
}
})
}
And it just works. Native code using obj.foo
won't notice a change (unless they check for the property descriptors but that's a weird thing to do). Reactive computations however will be invalidated by changes to this field.
However, it is weak against the module pattern (reference isolation to prevent corruption). Here's an example of such module :
(function logFoo(foo) {
console.log(foo);
}(obj.foo);
This code won't care that you changed the getter or the setter, it already holds the reference.
There might be a way around this... But as of this writing it's pretty much alchemy. An ES7 feature that could help: Object.observe
. It is so young today that I won't draw an example out of it.
If what you want to observe is not a non-moduled object field (example above), then the only solution I know of is polling.
Basically, regularly check if the value changed, and set a new reactive variable for that (we lose the transparency).
Example of such polling:
function reactivePoll(getter) {
var rPoll = new ReactiveVar(getter());
Meteor.setInterval(function pollVariable() {
var newValue = getter();
if(!_.isEqual(rPoll.curValue, newValue)) {
rPoll.set(newValue);
}
}, 100);
return rPoll;
}
What we need for it to work is not the variable reference in itself (foo
), but a getter to this variable. This is because if the foo
reference is later changed in the code our function won't be aware of it (still more painful desynchronized headaches).
Plus, we have to check for deep equality each time to make sure that the value did indeed change before we cause invalidations since Tracker will automatically invalidate if it sees a non-primitive value.
An example of use:
var reactiveFoo = reactivePoll(function getFoo() { return foo; });
It of course also works with object fields.
Note that the example code does not feature any kind of stop mechanism. It will run forever, may provoke memory leaks, crash, slow down your apps, and cause violent head pain. Do not use it as such in production applications, adapt it to add better control over the intervals.
The safest bet is then the basic dirty polling, even if it means a bit more load and a complete control needed to prevent memory leaks.
Upvotes: 6