Side Effects and Resource Cleanup
Sometimes you will have side effects associated with certain bind
s or
components, such as establishing a connection or setting an interval timer:
blinkingBox = (opts) ->
active = rx.cell false
setInterval (-> active.set not active.get()), opts.ms ? 500
div {class: bind -> if active.get() then 'red-box' else 'black-box'}
$('body').append(
div {class: 'container'}, boxes.map -> blinkingBox()
)
In these cases, you'll want to do proper resource management/cleanup
whenever the bind
context is removed or refreshed—in this case, stopping
the interval timer. For these situations, you can use
rx.onDispose(callback)
:
blinkingBox = (opts) ->
active = rx.cell false
interval = setInterval (-> active.set not active.get()), opts.ms ? 500
rx.onDispose -> clearInterval(interval)
div {class: bind -> if active.get() then 'red-box' else 'black-box'}
Garbage Collection
Dependencies hold references to their dependents in order to know who to propagate updates to. We do currently unsubscribe a dependent observable from its dependencies whenever that dependent is refreshed, but if the dependent observable itself creates nested binds, those nested binds must also be disconnected, and so on, recursively.
For instance, say we have:
showUnread = rx.cell(true)
unread = rx.cell(0)
div {class: 'scoreboard'}, bind -> [
if showUnread.get()
div {class: 'unread'}, bind -> "#{unread.get()} message(s)"
else
'(nothing shown)'
]
Initially, unread
has a subscriber, as expected: the inner bind
. When
we update showUnread
, the outer bind
will re-evaluate and create a new
inner bind
(for the nested div
), completely forgetting about the old
one. Without the nested bind cleanup, unread
now has two subscribers,
old and new inner bind
s. And since the old inner bind
is still
referenced from unread
's list of subscribers, it can't be
garbage-collected. Ergo, memory leak.
What instead happens: bind
tracks any nested bind
s. On re-evaluation,
these inner bind
s are disconnected, unsubscribing themselves from all
their dependencies. This disconnection also occurs recursively down to
their nested bind
s, and so on.
One question is what happens at the top level or when you want to break out and do your own thing. We don't have the luxury of weak refs in JS, and as a result it's less forgiving if you do "silly" things like create binds that go nowhere and that you don't want to keep.
(But even with weak references, one can't tell if you added a bind only to subscribe a console.log caller to its changes, save to localStorage, or some other side effect that you do want to keep.)
For instance, you don't want to write the above as the following:
showing = false
$('.toggle-button').click ->
if showing
$('.scoreboard').html(
div {class: 'unread'}, bind -> "#{unread.get()} message(s)"
)
else
$('.scoreboard').text('(nothing shown)')
showing = not showing
}
The key for these situations is that bind
s should go all the way to the
top, or else you'll want to manually disconnect your (top-level) binds
using their disconnect
method:
showing = false
topBind = null
$('.toggle-button').click ->
if showing
$('.scoreboard').html(
div {class: 'unread'}, topBind = bind -> "#{unread.get()} message(s)"
)
else
$('.scoreboard').text('(nothing shown)')
topBind.disconnect()
showing = not showing
}
Mutations Inside Binds
Say you had some dependent cell:
plusOne = bind -> src.get() + 1
If you try something like the following, i.e. explicitly wire up a dependent cell to modify a source cell:
bind ->
x = plusOne.get()
dst.set(val)
src.set(2)
you'll get a warning that you're trying to perform a cell-mutating operation within the subgraph of a dependent cell.
This is discouraged because a well-structured program should exhibit a
clean DAG of dependencies. When you do something like this, you're
potentially creating cycles. It also muddies what dst
should reflect,
when you have multiple bind locations calling dst.set(...)
—it's often
clearer to express the value of dst
as some expression of its
dependencies.
Usually you'll instead want to have dst
be a dependent cell of plusOne
rather than be its own SrcCell
:
dst = bind -> plusOne.get()
However, in case you need an escape hatch in a bind (har har), you can always set up a manual listener:
plusOne = bind ->
src.onSet.sub rx.skipFirst ([old, val]) -> val + 1
...
We need skipFirst
since by default the listener is immediately invoked
with the current value of src
.
Note that dst
will not be considered a dependency of src
or plusOne
.
This connection is unmanaged (or rather, managed by you).
So if this is happening within some bind context, you must also unsubscribe on context disposal:
uid = src.onSet.sub rx.skipFirst ([old, val]) -> dst.set(val)
rx.onDispose -> src.onSet.unsub(uid)
There is a convenience function, autoSub
, which will take care of this
for you:
rx.autoSub src.onSet, rx.skipFirst ([old, val]) -> dst.set(val)
Asynchronous Binds
So far we've seen synchronous binds. However, let's say you want to have introduce a delay between when a bind's dependencies are updated and when that bind is evaluated. This can be useful, for instance, if you have an expensive bind over some dependencies, where the dependencies can change in bursts of high frequency, and you don't want the app to block on re-evaluating the bind every time the dependencies change. Similarly, you may want a UI component to update in response to the user entering text into a text box, but you don't want a jarring user experience and want to instead only update the UI once the user stops typing for some time.
There is in fact a lagBind
:
y = rx.lagBind 500, 0, -> expensiveFunction(x.get())
Here, y
isn't immediately evaluated every time x
changes; rather, y
(which is initialized to 0) waits for x
to settle down for at least half
a second before evaluating the expensive function of x
.
There's also a similar function, postLagBind
, contributed by
@eizenberg, which instead introduces a lag but after the function
evaluation/re-capture (which is immediate upon any changes to
dependencies). This allows you to specify a dynamic delay as part of the
result, where the delay duration comes from observables. In this example,
the tootip appears instantly, but disappears only after a delay:
showTooltip = rx.postLagBind false, ->
if focused.get() and showTooltips.get() then val: true, ms: 0
else val: false, ms: 300
More generally, you can implement your own asynchronous binds. bind
,
lagBind
, and postLagBind
are all expressed in terms of asyncBind
.
You supply a function that runs the asynchronous
lifecycle. At a single point in the lifecycle, you can call
this.record(f)
, passing it a function which will record and subscribe to
dependencies. This can be called only once; otherwise things like disposal
of prior subscriptions gets very complex and multiple in-flight lifecycles
gets more complex (if you need multiple stages of bindings separated by
asynchronous gaps, you can use a chain of multiple asynchronous binds).
Upon completion, this.done(x)
must be called with the result.
Here's an example that immediately evaluates/records its dependencies and
then feeds that to an AJAX call; upon completion, the result is passed into
@done
.
y = rx.asyncBind 0, ->
input = @record -> x.get()
ajaxCall({data: input, done: (output) => @done(output)})
Users interested in asyncBind
are also encouraged to take a look at the
(very short) implementations of bind
, lagBind
, and postLagBind
.
ES5 Properties
So far, everything is built on observable data structures, which are just plain classes with explicit getter/setter calls. To make working with observables more natural/less verbose as well as more similar to working with regular non-observable data structures, we can use ES5 object property getters/setters, so that what used to be:
card = deck.cards.at(deck.cards.length() - 1)
card.isFlipped.set(not card.isFlipped.get())
becomes:
card = deck.cards[deck.cards.length - 1]
card.isFlipped = not card.isFlipped
Your class definitions are simpler as well. You can rewrite this:
class Deck
constructor: (player, cards) ->
@player = rx.cell(player)
@cards = rx.array(cards)
class Card
constructor: (suit, rank, isFlipped) ->
@suit = rx.cell(suit)
@rank = rx.cell(rank)
@isFlipped = rx.cell(isFlipped)
as:
class Deck
constructor: (@player, @cards) ->
rx.autoReactify(this)
class Card
constructor: (@suit, @rank, @isFlipped) ->
rx.autoReactify(this)
In short, you can construct "plain" objects, then autoReactify
them, which
replaces all fields with ES5 getters/setters (coupled with hidden backing
observable cells/arrays).
However, there are a few caveats and limitations, primarily around arrays.
Arrays are implemented by actually replacing the methods on normal Arrays
with wrappers that also call the appropriate method on the observable.
However, there is no way to override the indexing/subscript operator []
in JS. As a result, normal indexing operations may be less efficient (this
will likely be fine for most arrays/binds in your application, however, unless
they're really big):
deck.player # translates into deck.player.get()
deck.cards # translates into deck.cards.all()
deck.cards[0] # translates into deck.cards.all()[0], not deck.cards.at(0)
deck.cards.length # translates into deck.cards.all().length
# so, these won't work react to changes
cards = deck.cards
bind -> cards[0]
bind -> cards.length
# instead, always access as a field:
bind -> deck.cards[0]
bind -> deck.cards.length
All things considered, we still recommend using normal observable arrays
over these magical ES5 array proxies, given the limitations. When you call
autoReactify
on an object, it will essentially ignore and leave intact
any existing observables.
Thanks to @m1sta for bringing this idea to the table.