Fork me on GitHub
#re-frame
<
2023-10-13
>
Ernesto Garcia19:10:01

Hi, I'm defining this recursive subscription:

(rf/reg-sub ::app-db
  (fn [db]
    (println "calculating ::app-db")
    db))

(rf/reg-sub ::path
  (fn [[_ path]]
    (let [parent-path (drop-last path)]
      (if (seq parent-path)
        (rf/subscribe [::path parent-path])
        (rf/subscribe [::app-db]))))
  (fn [parent [_ path]]
    (println "calculating ::path" path)
    (if-some [k (last path)]
      (get parent k)
      parent)))
but, when subscribing to it...: • Each parent subscription is calculated twice • No subscription is being cached, everything is re-calculated

Ernesto Garcia19:10:57

As an example:

(rf/subscribe [::path [:user :username]])
calculating ::app-db
calculating ::app-db
calculating ::path (:user)
calculating ::app-db
calculating ::app-db
calculating ::path (:user)
calculating ::path [:user :username]
=> #object[reagent.ratom.Reaction {:val "ernesto"}]

p-himik19:10:27

The REPL is not a reactive context. I'm not sure why there's no warning for that, there should be this somewhere: "re-frame: Subscribe was called outside of a reactive context".

p-himik19:10:26

Also I would strongly recommend against that pattern in general. If you often find yourself creating subs from get-in-like paths, it's better to create a helper function for it that would let you register multiple subs and give them each a specific name. So instead of e.g. [::path :items 1 :sub-items 2 :title], you'd have [::sub-item-title {:item 1, :sub-item 2}] or something like that. ::sub-item-title would delegate to ::sub-item, which would delegate to ::item.

Ernesto Garcia20:10:25

> The REPL is not a reactive context. You're right, in a reactive context, each sub is calculated only once. (I was indeed getting the browser's warning).

Ernesto Garcia20:10:11

I like the recursive approach, as it will allow for doing an efficient subscription to an arbitrary path. Not sure if there is any downside to it...

Ernesto Garcia20:10:26

Actually, I'd prefer Re-frame offering efficient path subscriptions out-of-the-box. Something like Reagent cursors. Would a Reagent cursor on Re-frame's app-db interop well with Re-frame subscriptions?

p-himik20:10:55

> Not sure if there is any downside to it... Only from the perspective of the overall architecture. Nothing technical. That's why I suggested an alternative where you still get the potential efficiency but keep views untied from the underlying data structure. Reagent cursors should work just fine - after all, re-frame's subscriptions are plain reactions themselves (but wrapped in a cache, so for each sub vector there's only one reaction). And note that that potential efficiency gain by chaining multiple subs should still be measured since get-in is very fast, definitely significantly faster than using multiple gets, each inside its own reaction, with the whole propagation mechanism. What you gain or lose depends on the exact scenarios. You can easily gain 1 ms in one place and lose 10 ms in another.

Ernesto Garcia12:10:36

> Only from the perspective of the overall architecture. What do you think improves if using more elaborate subscriptions? > ...but keep views untied from the underlying data structure. Actually this is what I want to do, for reusable/rellocatable components. If you just pass a path to the component, it can use it as its own scope for both storing its data and observe its subscriptions. You could always pass it both a path and a subscription to that path... > Reagent cursors should work just fine I was wondering if Re-frame does anything special in order to not have glitches while propagating signals across the dependency tree, or if Reagent already takes care of that, and both are compatible in that sense, so that the Re-frame-Reagent combination is also glitch-free. > And note that that potential efficiency gain by chaining multiple subs should still be measured since get-in is very fast, definitely significantly faster than using multiple gets, each inside its own reaction, with the whole propagation mechanism. Only one get would be activated on a (usual) update. get-in is the one that would affect app's performance negatively, as it would trigger on every app-db update.

p-himik12:10:57

> What do you think improves if using more elaborate subscriptions? Not tying the views to the organization of the data. You'll change the layout of your app-db more frequently than you might realize now. Changing a single sub then is much easier than going to every view and changing it. In a similar way, your events shouldn't be just [:set :a :b :c 1]. It's worded much better somewhere in the re-frame docs. > If you just pass a path to the component, it can use it as its own scope for both storing its data and observe its subscriptions. Ah, a path to a component's is very different from a path to a particular datum. Instead of relying on paths for everything, I would suggest using component IDs. Then can be scalars, they can be vectors that are or aren't paths, but in the end components shouldn't care how that ID is used, they should only care about passing that ID to the relevant subs and events. > I was wondering if Re-frame does anything special in order to not have glitches while propagating signals across the dependency tree, or if Reagent already takes care of that, and both are compatible in that sense, so that the Re-frame-Reagent combination is also glitch-free. Glitches are possible, AFAIK the known ones (or just one?) have issues in the Reagent's repo. Re-frame doesn't add anything to the picture here. > Only one get would be activated on a (usual) update. With an (assoc-in db [:a :b :c] 1), with your approach four subs will be used - one for db and 3 for each path component, with each calling get.

p-himik12:10:09

But since it's not efficiency that you seem to care about the most, I would suggest settling on a good architectural decision and only then figuring out how it can be optimized, if any optimization is needed at all.

p-himik12:10:17

As an example, here's what I do for non-reusable components:

(def dlg-id :batch-update-rating)
(def dlg-path [:dialogs dlg-id])

(rf/reg-sub ::-dialog :-> dlg-path)
(rf/reg-sub ::instrument :<- [::-dialog] :-> :instrument)
(rf/reg-sub ::in-progress? :<- [::-dialog] :-> :in-progress?)
(rf/reg-sub ::file-name :<- [::-dialog] :-> :file-name)
(rf/reg-sub ::error :<- [::-dialog] :-> :error)
(rf/reg-sub ::ratings :<- [::-dialog] :-> :ratings)
(rf/reg-sub ::page :<- [::-dialog] :-> :page)
(rf/reg-sub ::page-size :<- [::-dialog] :-> :page-size)
(rf/reg-sub ::visible? :<- [::-dialog] :-> some?)
For reusable components it's very similar, it's just the signal function that needs to be changed (which is implicit in the code above with that :<-). The code above also uses my own wrapper for re-frame, but the only relevant difference is that in vanilla re-frame you can't write :-> dlg-path, you have to use :-> #(get-in % dlg-path).