re-frame

2025-01-29T15:24:39.624069Z

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.

2025-02-04T08:19:02.441329Z

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)

p-himik 2025-02-04T08:36:45.702949Z

No, that function doesn't write to the cache.

2025-02-04T08:37:45.452869Z

But the subscription is created, it has to be disposed somewhere at some point.

p-himik 2025-02-04T08:38:26.440549Z

What do you mean by that?

2025-02-04T08:44:11.082529Z

I mean that a subscription (i.e. a reaction) https://github.com/reagent-project/reagent/blob/master/src/reagent/ratom.cljs#L371-L377.

2025-02-04T08:47:40.940159Z

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.

p-himik 2025-02-04T08:48:23.424669Z

Why?

2025-02-04T08:49:16.360019Z

Because if a reaction is not disposed, it still watches something else upstream which will be prevented from being disposed.

p-himik 2025-02-04T08:52:04.375379Z

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.

2025-02-04T08:52:05.299479Z

Reagent disposes the reactions deref'ed in the render function of components, in their :componentWillUnmount function.

2025-02-04T08:54:40.362059Z

I see what you mean, I will check Reagent a bit more. Thanks.

p-himik 2025-02-04T08:55:27.768739Z

You can run a trivial experiment to confirm or invalidate what I'm saying.

p-himik 2025-02-04T08:57:34.269629Z

(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))

p-himik 2025-02-04T08:57:53.432059Z

(Ah, no, that's bad - hold on.)

p-himik 2025-02-04T08:58:19.850409Z

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))

p-himik 2025-02-04T08:59:23.306969Z

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.

p-himik 2025-02-04T09:00:23.156649Z

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.

p-himik 2025-02-04T09:15:39.843659Z

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.

👍 1
2025-02-04T09:27:54.033929Z

> 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. 👍

p-himik 2025-02-04T09:28:35.278579Z

Yep. :)

2025-02-04T14:16:00.612369Z

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.

p-himik 2025-02-04T14:17:21.436199Z

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.

👍 1
2025-02-04T14:17:55.795649Z

oops, I missed that part.

2025-01-29T15:25:55.512439Z

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?

p-himik 2025-01-29T15:29:07.154589Z

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.

👍 1
p-himik 2025-01-29T15:30:39.718709Z

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.

2025-01-29T15:32:59.154469Z

I would be interested by the source code, if you can share it.

2025-01-29T15:33:27.594669Z

I tried to do a custom solution, broke reactivity, re-implemented it, broke my spirit.

🤕 2
p-himik 2025-01-29T15:37:03.851419Z

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.

1
2025-01-29T16:57:53.139369Z

IIRC there is a link to something similar as a library, in the re-frame documentation about the motivation for flows.