The warning "Subscribe was called outside of a reactive context." is counter productive when there is a reason to call a subscription outside of a reactive context. It clutters the browser's console and make other warnings harder to be seen.
The user of uncached-sub needs to manually ensure that the subscription is disposed if it has no watchers: if it is not used in a reactive context, it won't happen automatically.
(if I understood correctly how it works)
No, that function doesn't write to the cache.
But the subscription is created, it has to be disposed somewhere at some point.
What do you mean by that?
I mean that a subscription (i.e. a reaction) https://github.com/reagent-project/reagent/blob/master/src/reagent/ratom.cljs#L371-L377.
Usually a subscription which is in the cache is removed from the cache once it is disposed.
When it is not in the cache (i.e. we didn't call re-frame.subs/cache-and-return on it), we still need to make sure it is disposed, implicitly by Reagent or manually.
Why?
Because if a reaction is not disposed, it still watches something else upstream which will be prevented from being disposed.
No. If something watches reaction X, then X will not be removed. In general, that "something" is a view that has deref'ed a sub. But then such a view also removed its watch when it's unmounted. If the deref is ourside of a reactive context, no watch is added - nothing to remove. If X depends on other reactive things, it won't matter if X is only used in non-reactive contexts.
Reagent disposes the reactions deref'ed in the render function of components, in their :componentWillUnmount function.
I see what you mean, I will check Reagent a bit more. Thanks.
You can run a trivial experiment to confirm or invalidate what I'm saying.
(def a (reagent.core/atom 1))
(let [r (reagent.core/reaction [:a @a])]
(js/console.log 'r @r))
(doseq [t [1000, 2000, 3000]]
(js/setTimeout (fn []
(swap! a inc))
t))(Ah, no, that's bad - hold on.)
This one:
(def a (reagent.core/atom 1))
(let [r (reagent.core/reaction (do (js/console.log 'a @a) [:a @a]))]
(js/console.log 'r @r))
(doseq [t [1000, 2000, 3000]]
(js/setTimeout (fn []
(swap! a inc))
t))If r were still "alive" when the last bodies of the timeouts run, you'd see multiple logged values of a. But you see only one - when the reaction is deref'ed the only time.
There is a way to preserve the reaction and make it run every time the values change - by using reagent.ratom/run!. It seems track! would also do that. But my code doesn't use them.
If that's not convincing, let's check the code - the only time a subscription in my code can do something, is when it's deref'ed. And this is the form that ends up being executed:
(if (and non-reactive (nil? auto-run))
(when dirty?
(let [oldstate state]
(set! state (f))
(when-not (or (nil? watches) (= oldstate state))
(notify-w this oldstate state))))
...)
The only potentially side-effecting things here are (f) and (notify-w ...).
f is the function we have provided ourselves - it's irrelevant.
And notify-w never gets called because a reaction deref'ed outside of a reactive context doesn't have any watchers, unless they're explicitly added.> If the deref is ourside of a reactive context, no watch is added - nothing to remove.
You are correct. If a deref happens outside of a reactive context, the derefs that may happen when (f) runs are still outside of a reactive context. And so on, recursively. 👍
Yep. :)
There is still no way to totally get rid of subscribe's warning, as it is called from https://github.com/day8/re-frame/blob/v1.4.3/src/re_frame/subs.cljc#L197-L212 in the sugar function.
As I said above:
> To make it work in layer-3 subscriptions, I also have a custom reg-sub that's a copy of rf/reg-sub but also has that (if *cache-subs* rf/subscribe uncached-sub) in there.
oops, I missed that part.
I found no easy way to avoid triggering it, and it doesn't look like it can be manually turned off. Is there any chance that we can turn it off in the next release?
There already is an issue for it: https://github.com/day8/re-frame/issues/753 IMO just turning it off is worse than to have it on for all cases.
And while there is no easy way of avoiding triggering it, there still is a way. In my own projects, I have a custom subscribe that doesn't write to the cache and doesn't warn about such usages if a specific dynamic variable is set. That variable is then set by a wrapper function that's used by an interceptor that injects subscription value into the coeffects map.
I would be interested by the source code, if you can share it.
I tried to do a custom solution, broke reactivity, re-implemented it, broke my spirit.
There's very little to it, it's just a thinned down copy of rf/subscribe.
(defn- uncached-sub [query]
(if-some [cached (rf-subs/cache-lookup query)]
cached
(let [query-id (rf-utils/first-in-vector query)
handler-fn (rf-reg/get-handler rf-subs/kind query-id)]
(if (nil? handler-fn)
(console :error (str "re-frame: no subscription handler registered for: " query-id ". Returning a nil subscription."))
(handler-fn rf-db/app-db query)))))
(def ^:dynamic *cache-subs* true)
(defn maybe-cached-sub [query]
(let [f (if *cache-subs* rf/subscribe uncached-sub)]
(f query)))
It doesn't break reactivity because it doesn't rely on reactivity in any way, since uncached-sub is used only in non-reactive contexts by explicitly setting *cache-subs* to false.
To make it work in layer-3 subscriptions, I also have a custom reg-sub that's a copy of rf/reg-sub but also has that (if *cache-subs* rf/subscribe uncached-sub) in there.IIRC there is a link to something similar as a library, in the re-frame documentation about the motivation for flows.