Fork me on GitHub
#re-frame
<
2021-12-25
>
or20:12:00

Hallo, folks. I've found myself in a situation where I want/need the signal inputs function of a subscription registration to return subscriptions that in some way depend on state in the db. I think there are legitimate cases where this is useful. In my case I want to wrap dynamic forms/validation around nested data, and a subscription for a given path into the structure should return the relevant data for whatever entity is found in the db where the path points. But that data and the subscriptions it relies on can change when the type of that entity changes, so the input signals can't be static. I've created a simple test case in re-frame to illustrate what I mean:

(deftest test-reg-sub-macro-conditional-input-signals
  (subs/reg-sub
    :condition-sub
    (fn [db [_]] (:condition db)))

  (subs/reg-sub
    :a-sub
    (fn [db [_]] (:a db)))

  (subs/reg-sub
    :b-sub
    (fn [db [_]] (:b db)))

  (subs/reg-sub
    :conditional-a-b-sub
    (fn [_ _ _]
      (if @(subs/subscribe [:condition-sub])
        (subs/subscribe [:a-sub])
        (subs/subscribe [:b-sub])))

    (fn [value [_]]
      {:result value}))

  (reset! db/app-db {:condition true :a 1 :b 2})
  (let [test-sub (subs/subscribe [:conditional-a-b-sub])]
    ;; passes
    (is (= {:result 1} @test-sub))

    (swap! db/app-db assoc :condition false)
    ;; fails
    (is (= {:result 2} @test-sub))))
This test fails, and I believe I understand why. It's not a bug, it's just something that shouldn't be done. The first time the subscription is created :condition true holds in the db, so a subscription to :a-sub is tracked as a signal input to the subscription. But when the state updates to :condition false that input subscription (or the signal graph) doesn't change, so the result remains {:result 1}. I also considered returning the conditional subscription itself as a hack, so it becomes one input in the signal graph, but that doesn't help either, as the input signals function isn't run again on input changes. I currently solve it by moving the conditional subscription outside of the subscription registration, passing its value as an argument, so the input signals function can use it, and if the argument changes, then it's a new subscription anyway. However, this might require re-computation and re-rendering in many cases where it wouldn't have been necessary. It also makes it hard to use that subscription as an input signal for any other subscription, as the required conditional subscription needs to be passed in as an argument at the beginning of the chain, and code all over the place needs to know about this. I want to attempt a patch that optionally allows another layer of a function that generates signal subscriptions that result in recomputation of the input signals, if required, but maybe there's a smarter way I missed. Before I reinvent the wheel or come up with a shape of a wheel that nobody needs: has anyone required this sort of case before and maybe found a better way to handle this? Or might it be worth doing?

p-himik20:12:20

Your analysis is correct - the signal function is not part of the reaction created by reg-sub (or rather, by the function it creates, but that's details). So whatever you deref in there, will preserve the value obtained with the first subscribe. It might be recomputed later if the subscription is evicted from the cache. The proper way to do it would be to use reg-sub-raw. That's it, you don't need anything else. Some things I want to note that aren't directly related to your problem: • subs/* makes me think that you're using re-frame.subs namespace directly. Don't do it. The only public API in re-frame is re-frame.core. Just require [re-frame.core :as rf] and use rf/reg-sub, rf/subscribe, etc • Don't directly access app-db - even in tests. Instead, use dispatch-sync, like described here: https://github.com/day8/re-frame/blob/master/docs/Testing.md • Usage of subscriptions outside of reactive contexts (like views or other reactions that are used in views) may result in multiple calls to the same subscription handlers even when the values haven't been changed. That's expected but I'm not sure what would be the best way to fix it • You're using a 3-arity function as a subscription signal in :conditional-a-b-sub. That's incorrect, there are only two arities, 1- and 2-. And the 2- one is deprecated so use just (fn [_] ...) in there • Finally, you use [value [_]] as a signature. A truly minor thing, but it can be simplified to [value _]

or21:12:46

Thanks for your response, I haven't checked reg-sub-raw, I'll do that. As for the other points: I agree with all of them, I only copied one of the tests in test/re_frame/subs_test.cljs and adjusted it. That is the style they use.

p-himik22:12:42

Well, tests of re-frame can use all the internals they want, since they're part of re-frame. :)

or22:12:29

Yep. But I added it as a re-frame test, in that namespace. 🙂

or22:12:43

Looks like reg-sub-raw will do the thing. Excellent. A slight disadvantage might be, that everything is done in one function. So any change to one of the inputs will result in re-computation of what inputs are needed and what to do with them, if that makes sense. I'll give it a proper try to see whether that's a performance problem.

or22:12:29

To clarify: in my model above the input signals would only have to be computed if any of the conditional inputs change. Now it would always have to be redone if any of the inputs change. I /think/ in my case this should be acceptable, tho, as that operation should be quick.

p-himik22:12:51

No, I mean that re-frame as a library has its own tests - they can use private API just fine. Your tests are, unless you're writing a PR for re-frame that add tests or maintaining a fork with additional functionality, are those of your app/library - not of re-frame. Regardless of the namespace. > any change to one of the inputs will result in re-computation of what inputs are needed Something that's not used (like in the wrong branch of if) will not be recomputed. Only the things that are used are computed and recomputed again. So if you have (if false (subscribe [:a]) (subscribe [:b])), the :a sub will never be computed in the first place, and will never be recomputed.

or22:12:03

I understand, but the "false" changing (if it's based on a subscription) would result in that whole function being re-evaluated. And that computation of finding the right subscriptions might be pricy.

or22:12:51

And I'm not expressing myself well, it seems. I forked re-frame and added a test in the re-frame test suite, based on an existing test there. 🙂 In preparation of trying to write code in re-frame.

p-himik22:12:04

Ah, I see what you mean. Yes, if that's the problem just use one intermediate reaction. Or rather, use reg-sub-raw with if inside it to create a sub for an input signal and then use that signal in a regular reg-sub. Ah, so you're extending re-frame, alright. Then yeah. those tests should be written in the same manner.

or22:12:52

Aye, but it definitely is a step in the right direction. Not sure why I didn't see it in the docs.

p-himik22:12:51

It might not be there at all! Might be worth a PR.

or22:12:12

There is some, I found it after your suggestion. Don't think it was linked from the normal subscription docs. I can double check. Perhaps a "DON'T DO THIS!" section could be helpful as well.

or22:12:21

I'm off for now. Thanks again!

👍 1