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-calculatedHi Eugene, sorry for the delay in responding:
> Not tying the views to the organization of the data.
This is actually the intent. By passing paths components are decoupled from the app-db concrete schema. Subscriptions and events just take the path and add to it.
> Instead of relying on paths for everything, I would suggest using component IDs.
I don't see how component IDs help in you proposal. The purpose of paths is to provide the component a scope where it can place its data. How would you get that from component IDs?
> they should only care about passing that ID to the relevant subs and events.
I see... this is the intent with paths as well, that they are just passed to subs and events opaquely to the component.
> 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.
Of course, you're totally right. Not sure what I was thinking. The modified branch of the persistent map will change at every level. This means that Re-frame doesn't to a good job in being efficient, as there is no efficient approach.
> But since it's not efficiency that you seem to care about the most
I do care a bit; I might reach a level where efficiency is needed. That's why I was asking for Reagent cursors.
Thank you for sharing your example. In our case, we usually introduce a new path for a component in its dedicated model namespace. We call it a scope. For subscriptions we just define the parent ::scope subscription in that namespace. For events we define a scope function taking a path-suffix, for building the absolute app-db path for storing the component's own data.
::scope may be parameterized. In that case, we define a signal helper function <-scope that takes the needed parameters, and can be used for defining further subs.
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"}]Why?
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".
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.
> 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).
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...
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?
> 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.
> 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.
> 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.
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.
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).