Fork me on GitHub
#re-frame
<
2023-03-31
>
DrLjótsson03:03:21

How do you spec and instrument your re-frame events and subs? I've gone all in on instrumenting my backend db queries using malli. On the frontend, I often feel (momentarily) lost when trying to figure out the shape of the data that I am expecting. Do you extract your events that receive data over http as functions and instrument their arguments and similarly extract your subs as functions and instrument their return value?

DrLjótsson03:03:10

Nah, I just saw this post on Clojureverse. Instrumenting and extracting the functions won't work unless I call them "indirectly" in the subs and events https://clojureverse.org/t/re-frame-subs-and-spec-test-instrument/3903/4

Rachel Westmacott09:03:31

I have a question. In a view (A) in my app I want to display some data (2) which is fetched from a server. In order to fetch it I need to know another piece of data (1) which also comes from a server. Data 1 may or may not already be available in my app-db. If it isn’t, it will be soon. When the user navigates to view A I want to fetch data 2 so I can display it. But I can’t fetch it until data 1 has arrived. What is the best mechanism to register an interest in fetching data 2 once data 1 is available? So far I’ve considered creating a recursive reg-event-fx that checks the app-db for data 1 to see if it can dispatch the event that fetches data 2, and if it can’t then it uses :dispatch-later to call itself in the future and retry. I’ve also considered subscribing to data 1 and adding a watch to it and dispatching the event that fetches data 2 when data 1 arrives. Any thoughts on these or other approaches? Have I missed an obvious mechanism here?

👍 2
p-himik09:03:32

Since your requests need to collaborate, you have to have some medium over which they can collaborate. With re-frame, app-db is the obvious choice for storing that data-1. What is less obvious is that you also need to store the information about which requests are in flight if they're a dependency of some other request. But this information can go also to app-db or to some other atom if you prefer. A third thing that you need is to be able to register a need for a new request if one of its dependencies are in flight. With just one dependency it's trivial and you don't need to create a whole system for this request dependency management. With less trivial dependency graphs, perhaps dynamic rule engines will be useful here, but I don't have any experience with them myself.

mikepjb09:03:43

Not sure if I understand fully but can you chain subscriptions like this?

(reg-sub
 :data-2
 (fn [_] ;; assuming no params required
   [(subscribe [:data-1])]) ;; you can add more inputs here if required

 (fn [[a] _]
   (if a
     (collect-data-2 a)
     {:data :no-data} ;; or whatever you want the nil state to be
     )))

mikepjb09:03:08

I mean you can do this.. but I am wondering whether this addresses your issue or whether something has been missed

p-himik09:03:35

The issue is not about subscriptions but about events.

Rachel Westmacott09:03:17

@U0DMK0TAR does that require me to dispatch an event in collect-data-2? Isn't it bad practice to dispatch events in subscriptions?

mikepjb09:03:48

Like @U2FRKM4TW says it’s not good practice but I guess it depends how much data is being retrieved for data-2.. you can always dispose of the subscription with reg-sub-raw if this is a one time thing. This gets it working at least, I am not sure what the correct answer would be here but interested to see the solution you end up with!

Rachel Westmacott09:03:23

do you (or anyone else) see any problems with the two approaches I've outlined above? (a recursive event handler with dispatch-later, or a watch)

mikepjb09:03:58

Have you tried them out? I like the sound of the watch approach best just because of the recursive calling in the alternative

Rachel Westmacott10:03:27

Recursive event handler looks like this:

(rf/reg-event-fx
  ::when-ready
  (fn [cofx [event-kw {:keys [cofx-ready? event ms limit]
                       :or {ms 100 limit 10}
                       :as event-config}]]
    (cond
      (cofx-ready? cofx) {:fx [[:dispatch event]]}
      (pos? limit) {:fx [[:dispatch-later {:ms       ms
                                           :dispatch [event-kw (assoc event-config
                                                                 :ms (* 2 ms)
                                                                 :limit (dec limit))]}]]}
      :else {:fx [[:dispatch [::log/error (str "Never ready to dispatch event: " event)]]]})))
and calling it looks a bit like:
(rf/dispatch [::utils/when-ready {:cofx-ready? #(get-in % [:db ::nsa/data-1 :important-id])
                                  :event       [::nsb/fetch-data-2]}])

p-himik10:03:07

The recursive event approach is just a busy loop - it does more work than needed and creates an unnecessary delay.

Rachel Westmacott10:03:41

The watch approach currently looks like this:

(let [!data-1 (rf/subscribe [::nsa/data-1])
      !data-2 (rf/subscribe [::nsb/data-2])]
  (add-watch !data-1 :fetch-data-2 (fn [watch-key reference _old new]
                                     (when new
                                       (remove-watch reference watch-key)
                                       (when-not @!data-2
                                         (rf/dispatch [::nsb/fetch-data-2])))))
  (when @!data-1 (rf/dispatch [::nsb/fetch-data-2])))

p-himik10:03:01

Also, it doesn't dispatch the first event, it just waits for it.

2
p-himik10:03:15

I mean, what if your data-1 event is never dispatched in the first place? Then the data-2 event will be waiting forever, instead of asking for the required piece of data itself.

Rachel Westmacott10:03:43

In this case it is the job of some other code to ensure data-1 arrives.

p-himik10:03:59

If the first event is always issued, then a better approach is to simply use a global interceptor.

Rachel Westmacott10:03:06

There is also a time limit - it will give-up eventually.

Rachel Westmacott10:03:55

data-1 should always be fetched - but depending on the user's route through the app data-2 may not be needed.

Rachel Westmacott10:03:12

if data-2 is needed, we may know that before data-1 has arrived

Rachel Westmacott10:03:00

I'm not familiar with global interceptors.... /heads to internet

p-himik12:03:10

Don't use it for this kind of thing - plenty of discussions of "why" here.

Rachel Westmacott12:03:17

@UF8TR5VHT that looks great. @U2FRKM4TW what am I missing?

p-himik12:03:32

Please search in this channel - as I said, there have been plenty of discussions, with a lot of useful info particularly from emccue.

ack 2
Rachel Westmacott13:03:48

for those interested I think this thread is the sort of thing @U2FRKM4TW is referring to: https://clojurians.slack.com/archives/C073DKH9P/p1643901164589709

Rachel Westmacott13:03:34

Unless someone tells me this is stupid, then I've probably settled on something that seems to work. First we dispatch an event to say that data-1 is needed for us to fetch data-2.

(rf/dispatch [::data-1-needed-for [::fetch-data-2]])
Then we handle that event by checking to see if we have data-1 - if we do then we can dispatch the ::fetch-data-2 event, otherwise we record in the app-db that we will want to dispatch it once data-1 is available.
(rf/reg-event-fx
  ::data-1-needed-for
  (fn [{:keys [db]} [_ subsequent-event]]
    (if (get-in db [::data-1])
      {:fx [[:dispatch subsequent-event]]}
      {:db (update db ::data-1-needed-for conj subsequent-event)})))
Then in the success handler for fetching data-1 we can dispatch anything that may still need to be dispatched.
(rf/reg-event-fx
  ::data-1-fetch-success
  (fn [{:keys [db]} [_ result]]
    {:db (-> db
             (assoc ::data-1 result)
             (dissoc ::data-1-needed-for))
     :fx (concat [[:dispatch [::some-other-thing]]]
                 (map #(vector :dispatch %) (::data-1-needed-for db)))}))

👍 1
p-himik14:03:08

Yeah, that's pretty much the approach if you're fine with making the data-1 event be aware that it's a dependency of other events. Although, and it's just a tiny thing, the last :fx I'd write as:

(into [[:dispatch [::some-other-thing]]]
      (map (fn [evt]
             [:dispatch evt]))
      (::data-1-needed-for db))

👍 2
Rachel Westmacott14:03:57

because you prefer into over concat due to laziness?

Rachel Westmacott14:03:30

or to remove the intermediate collection created by calling map?

Rachel Westmacott14:03:03

or to ensure the result is a vector?

p-himik14:03:41

Due to laziness and the fact that concat will blow up if used recursively. I prefer not to think about where I should and shouldn't use it so I resolve to using it only when other alternatives are much less desirable. Also, one can use cons instead.

👍 2
Rachel Westmacott14:03:39

in this example yes, but in fact there are two ::some-other-things, so in my actual code cons wouldn't work (unless I called it twice)

Rachel Westmacott14:03:05

Thank you for your input, @U2FRKM4TW (and others)

👍 4
Emanuel Rylke14:03:31

We've been solving some performance problems by moving subscriptions from the computation fn into the signals fn inside our reg-subs. My first question: is there some writeup somewhere on why this improves the performance? From reading the re-frame docs and doing a little experimenting a while ago I had gotten the impression that using a signals fn was just about code readability, but that's clearly not the case. Second question: we still have a few reg-subs with essentially this pattern:

(reg-sub :foo
  (fn [[_ x] _]
    (subscribe [:bar x]))
  (fn [bar _]
    (doall (map #(deref (subscribe [:quux %])) bar))))
is it fine to deref subscriptions inside the signals fn? like this:
(reg-sub :foo
  (fn [[_ x] _]
    (mapv #(subscribe [:quux %]) @(subscribe [:bar x])))
  (fn [quuxed-bar _]
    quuxed-bar))
or is there some other way to do subscriptions for which the arguments depend on other subscriptions?

p-himik14:03:59

> is there some writeup somewhere on why this improves the performance? Pretty sure it's in the official documentation of subs or in the advanced topics. tl;dr: caching along with the fact that recomputation of a signal-using sub is triggered only when one of its signal's value changes as opposed to whenever app-db changes. > is it fine to deref subscriptions inside the signals fn? It all ends up being in a single reaction, so no big deal. However, that's a pretty strong smell of "you should probably use reg-sub-raw instead".

🙇 2