Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding on-dispose callbacks to a Reaction #201

Closed
mike-thompson-day8 opened this issue Oct 30, 2015 · 12 comments
Closed

Adding on-dispose callbacks to a Reaction #201

mike-thompson-day8 opened this issue Oct 30, 2015 · 12 comments

Comments

@mike-thompson-day8
Copy link
Member

At the time you create a reaction you can supply in an on-dispose callback. All good.

But I have a case in re-frame where I'd like to add an on-dispose callback sometime after the creation of a Reaction.

Implications:
- a given Reaction might now have more than one on-dispose. A list?
- on-dispose becomes a mutable field
- there'd have to be an API for adding on-dispose callbacks.

@erikprice
Copy link

As opposed to just closing over an atom?

@mike-thompson-day8
Copy link
Member Author

@erikprice I don't follow. Could you elaborate?

@erikprice
Copy link

Well, maybe I'm missing something. But I was thinking you could use the standard on-dispose callback, and pass in a function which closes over an atom when the reaction is created. When the callback is called, it just calls the atom if it's not nil. You can set the atom to the "real" callback sometime after the creation of the reaction.

@mike-thompson-day8
Copy link
Member Author

Just to be clear: I'd like to add a new method, add-on-dispose!, to the deftype Reaction.

That's because I have code which is given a Reaction and it needs to know when that Reaction is no longer required.

(let [some-reaction  (some-fn)
      my-callback-fn  #(...)]
  ...)    ;;  <---  I want to do this  (add-on-dispose!  some-reaction my-callback-fn)

So this adding of a callback has to happen sometime AFTER the Reaction is created. At the moment, a single on-dispose callback can be supplied at creation time.

@mike-thompson-day8
Copy link
Member Author

@holmsand what do think? This is pretty important because it will allow re-frame to auto de-duplicate subscriptions (think readonly cursors). If there are 100 subscriptions to the one part of the state, there should only be one Reaction observing it, not 100.

I'm hesitating on doing a PR because I notice a lot of code changes in play (in this area) and I'm not sure of your current intent/timing etc. I'm guessing that you are experimenting on something (I can't quite discern overall outcome yet).

@holmsand
Copy link
Contributor

holmsand commented Nov 3, 2015

@mike-thompson-day8 There are actually two ways to do what (I think) you want in 0.6:

There's a new macro, reagent.core/with-let, that works like let, except that the bindings are only evaluated the first time a component is used, and it takes an optional finally clause that is called at "dispose time". In other words, you can have a component like this:

(defn foo []
  (r/with-let [value (r/atom 0)
               interval (js/setInterval #(reset! value (js/Date.now)) 1000)]
    [:div
     @value]
    (finally
      (js/clearInterval interval))))

That component will show the current time stored in value, that is updated until that component is no longer used.

with-let can be used both in components, and in Reactions generally, so it provides a generalised way to handle resources that need to be created before use, and destroyed when they're no longer needed.

The second new feature, reagent.core/track, is a much more straight-forward way to do what I think you want.

Given a function f, (r/track f x) returns a derefable value, that executes f in a Reaction with parameter x. If you use track more than once with the same function and the same parameters, only one Reaction will be created. So, in general, you can use @(r/track f args...) as a way of caching the results of a function that depends on atoms and/or reactions. In other words, it can be used for "read-only cursors" directly.

Since "tracked functions" use Reactions, you can also combine them with with-let. For example, given foo above, @(track foo) will only create one atom and one interval even if used in multiple components or reactions.

@mike-thompson-day8
Copy link
Member Author

@holmsand the new features look interesting, but neither would solve my need. (I have a general problem for which track is too specific).

@holmsand
Copy link
Contributor

@mike-thompson-day8 I've had a go at adding add-on-dispose! to Reaction (now on master). It adds a function to a new on-destroy-arr array on the reaction object. The function is called (with no arguments) after the original on-destroy function is (or would be) called.

Take a look – since I (obviously) didn't quite get your use case, let me know what you think!

@mike-thompson-day8
Copy link
Member Author

@holmsand Thanks! It is looking good so far. Will report back further in the next day or so.

@mike-thompson-day8
Copy link
Member Author

mike-thompson-day8 commented Jun 4, 2016

@holmsand So, this all works as advertised. And I'm happy. Many Thanks.

Implementation

I note the following:

  • I would have expected add-on-dispose! to be a part of the existing IDisposable protocol rather than a new IReaction protocol.
  • Reaction has a slightly confusing mixture of explicit fields like state and caught, mixed with non-explicit "option" fields like on-set, on-dispose and the new on-dispose-arr. Had me puzzled initially.

Conceptually

For any future users of these "dispose" related features, I note that: dispose related callbacks are not guaranteed to be called once. In certain cases, they might not be called at all and, in other cases, they may be called multiple times. Something to be aware of.

Further explanation: a reaction is "disposed-of" when its number of watchers makes the 1 to 0 transition. Ie. it goes from being watched, to no longer being watched.

As a result, a reaction which is never watched once, will never be disposed of (because it can't make the 1 to 0 transition, it was only ever at 0 watchers).

This scenario can arise when a reaction is used conditionally within a downstream reaction like:

(reaction (if  @this-is-bool @t-reaction @f-reaction))   

Assuming @this-is-bool is always true, f-reaction is never captured/watched and is thus never disposed of (assuming also nothing else ever watches it).

Also, in certain cases, a reaction can be disposed of multiple times. It is conditional use that creates this scenario. In the code above, if the value in this-is-bool alternates from true to false and back again, then f-reaction will transition from 0 watches to 1 and then back to 0, etc. That will cause the "dispose-related" callbacks to fire regularly (more than once!).

None of this is a train smash, but it may be surprising to those using this feature.

@holmsand
Copy link
Contributor

holmsand commented Jun 7, 2016

@mike-thompson-day8 Good point about IDisposable vs IReaction. I've done the move.

I've also had another think, and changed the call to the "destroyer" function: it now gets the reaction as an argument (which may be useful, and is similar to the "normal" on-destroy).

About the implicit fields: that is me trying to minimise memory use by avoiding allocation of less frequently used fields. Probably overzealous... I'll try to do some benchmarking someday to check it out.

You're of course absolutely correct about on-dispose not being called exactly once. In case anyone wonders, this is by necessity. Since there are no finalizers in javascript, there's no way to know when a created reaction ceases to exist – so we cannot call the on-destroy function on a reaction that's never been used. Our only chance to do that is when the reaction no longer has any watches, so that is when on-destroy is called – even if the reaction may become active again later.

@mike-thompson-day8
Copy link
Member Author

Delivered as part of v0.6.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants