Fork me on GitHub
#re-frame
<
2021-04-22
>
Rebecca Bruehlman00:04:23

Can someone point me to a good resource (or just ELI5 😉) when you would want to use reg-fx vs. reg-event-fx? Haven’t found many useful examples. Does it sound right to say that: reg-fx might be useful in cases where you have, say, a reg-event-fx that is firing off multiple effects that are only invoked by that event (so the dispatching event can grab items from the db and cofx and pass along as necessary)? Or, alternatively, where there is some kind of effect that has nothing to do with the db or what is contained in cofx (e.g., making an ajax request)?

lassemaatta04:04:40

The effect handler in reg-event-fx should be pure (=no side-effects). If you need to do side-effects (e.g. write a value to local storage, set a timer etc), you can use reg-fx to do the actual work and refer to it in the pure effect handler.

jacekschae04:04:19

The way you think about this is pretty close to what I also have in mind. I would use reg-fx when I need to do something outside of the app-db. A good example is :http-xhrio. Another one might be navigation and this is where I would often reg-fx and hook it up to reg-event-fx. The same thing goes for thing such as copy to clipboard, local storage …

;; -- navigate-to --------------------------------------------------------------
;;
;; Using reitit frontend router, rfe is `[reitit.frontend.easy :as rfe]`
(rf/reg-fx
 ::push-state
 ;; Sets the new route, leaving previous route in history. For route details
 ;; see `:navigate-to` fx.
 (fn [route]
  (apply rfe/push-state route)))

(rf/reg-event-fx
 :navigate-to
 ;; `route` can be:
 ;; * a single route: a keyword or a string
 ;; ex. `::home` or `/home`
 ;;
 ;; * a route with params
 ;; ex. `::org :path-params {:org-id 2}`
 ;;   `/org/2`
 ;;
 ;; * or a route with params and query params
 ;; ex. `::org :path-params {:org-id 2} :query-params {:foo bar}}`
 ;;   `/org/2?foo=bar`
 (fn [_db [_ & route]]
  {::push-state route}))
Hope that helps, again this is just my point of view

Rebecca Bruehlman13:04:12

@UC506CB5W yep, seen that doc, hence why I asked because butterfly was too abstract, haha. You mention avoiding side-effects--I get putting in timeouts and local storage and the like, but let’s say I want to modify something on the DOM. Would something like this qualify as something that should be put in reg-fx?

(let [video (.querySelectorAll js/Document '#video')]
(set! (.-srcObject video) some-stream))
(I suppose I could also store the stream in the app db and have the component subscribe to changes… maybe that is the better pattern?)

Rebecca Bruehlman14:04:52

@U8A5NMMGD I think this makes sense, although the effects are called unordered, right? (as opposed to how dispatch will call event sequentially). How do you handle that? I suppose a way to think about it is that the only logic in reg-event-fx should be manipulating data objects that are returned in the map (whether that’s dispatching event A if foo is true vs. event B if foo is not true, or modifying the db or something)…

lassemaatta14:04:53

I think the word you should watch out for is "modify", as I believe that implies it being side-effectful. That is, the function affects the world in some other way than just by returning a value. In a pure effect-handler for reg-event-fx (or reg-event-db) you don't actually modify the app db, instead you return a new value.

Rebecca Bruehlman14:04:51

correct, sorry. My Python is coming through with the verbiage 😉 I know the app db is entirely replaced

jacekschae15:04:13

@rbruehlman I’m not sure if I follow. The example with the route navigation is one reg-fx and one reg-event-fx that uses the reg-fx so you would call that in your :dispatch or these days with :fx

{:db ...
 :fx [[:dispatch [:navigate-to :my-route]]}
within :fx the ordering is sequential

Rebecca Bruehlman15:04:42

Right, but :navigate-to is the event-fx. What if navigate-to had multiple effects, like:

(rf/reg-event-fx
 :navigate-to
 (fn [_db [_ & route]]
  {::push-state route
   ::some-other-fx nil}))
The order wouldn’t be guaranteed. Or is that a code smell?

jacekschae15:04:19

if you would like the order to be guaranteed you could dispatch them with :fx . Plus I don’t think that if order doesn’t matter it’s a code smell

Rebecca Bruehlman00:04:20

On a related note, manipulating, say, the srcObject of a video element feels like an “effect” to me, but it also feels sort of …. meh…. “wrong” to put it in reg-event-fx because it is not using anything that event handler supplies. is this where reg-fx might be helpful, or no?

lilactown14:04:17

fxs are best used when dealing with global side effects outside of the UI, things like data fetching or reading and writing to local storage

lilactown14:04:02

I would keep any manipulation of the UI in reagent/react and let re-frame handle business logic and global app side effects

Rebecca Bruehlman13:04:58

looping back around on this …. So, if I shouldn’t manipulate srcObject with reagent and should instead let reframe handle setting the video stream, then…. I rewrote what I had to use subscriptions instead:

(defn video-stream [stream]
    [:div {:style {:overflow "hidden"
                   :max-width 500
                   :max-height 500
                   :width "100%"}}
     [:video
      {:auto-play true
       :srcObject stream
       :style {:max-width (:width 500)
               :max-height (:height 500)
               :width "80%"
               :height "80%"
               :position "relative"
               :left "50%"
               :top "10%"
               :transform "translate(-50%, 10%)"
               :object-position "center top"}}]])

(defn start-stream []
  (rf/dispatch [::vc-events/set-video-stream :local-stream]))

(defn video-chat-app []
  (let [stream @(rf/subscribe [::vc-subs/stream :local-stream])]
  
  (if (nil? stream)
    (start-stream)
    (video-stream stream))))
(defn get-stream []
  (-> js/navigator
      .-mediaDevices
      (.getUserMedia #js {:video true :audio true})))

(rf/reg-event-db
 ::set-video-stream
 (fn [db [_ type]]
   (vc-q/set-stream db type (get-stream))
))
However, because getUserMedia returns a promise, setting my component’s srcObject property literally sets said property to a promise that does not resolve (whereas setting it with (set! (.-srcObject element) stream) resolves said promise). So I must still be doing something wrong 😉 What is the right way to trigger displaying streaming video with reagent, if the proper way to go about things is to use subscriptions?

p-himik13:04:48

You would have to call .then on that promise, and in the passed function call dispatch to store the actual stream in the DB.

p-himik13:04:12

Also, you should rewrite your video-chat-appcomponent: - Don't call dispatch at the top level of the rendering function (via start-stream in this case): https://day8.github.io/re-frame/FAQs/LoadOnMount/ - It's likely that you want to use [] instead of () with video-stream: https://github.com/reagent-project/reagent/blob/master/doc/UsingSquareBracketsInsteadOfParens.md

p-himik13:04:39

> if I shouldn’t manipulate srcObject with reagent I think lilactown meant exactly the opposite - you should do it with Reagent, when it makes sense. Use re-frame for everything that you yourself consider your app's state. Whether "video stream from a web cam that should be displayed in a <video> tag" is part of your app's state or not - up to you. And of course, sometimes such an ideal still breaks because of how some JS APIs are written - in that case, use whatever's feasible.

emccue14:04:40

@rbruehlman For me, reg-event-fx always returns :db and :fx

emccue14:04:33

(defn handler:some-event
  [{:keys [db]} _]
  {:db (... update state ...)
   :fx [ (... effects in order ...) (...) (...) ]})

(rf/reg-event-fx
  event:some-event
  handler:some-event)

emccue14:04:08

and it doesn't perform side effects

emccue14:04:00

so you kick off any "side effects" you want to perform as a result of the call directly

emccue14:04:24

those "side effects" are ultimately supplied by reg-fx

emccue14:04:11

which you should need fairly few of for your whole app

emccue14:04:26

and we also are moving to never using dispatch

p-himik14:04:29

What are you using instead?

emccue14:04:42

just flattening events

emccue14:04:04

so like, lets say you had this

emccue14:04:00

(rf/reg-event-fx
  :event-a
  (fn [{:keys [db]} [_ data]]
    {:db (update-a db data)
     :dispatch-n [[:event-b data] [:event-c data]]}))

(rf/reg-event-db
  :event-b
  (fn [db [_ data]]
    (update-b db data)))

(rf/reg-event-fx
  :event-c
  (fn [{:keys [db]} [_ data]]
    {:db (update-c db data)
     :http-xhrio { ... }}))

emccue14:04:08

this kinda work as an example?

p-himik14:04:03

I'm afraid I still don't follow. That's exactly what I've been doing from the very start. Were you calling dispatch from event handlers previously?

emccue14:04:18

no this is the counter example

emccue14:04:34

i'm just starting with this to show how we transform it

emccue14:04:50

just making sure we are at the same starting point

emccue14:04:45

so if you assume all of event-a, event-b, and event-c are being dispatched somewhere in the code

emccue14:04:59

we start by moving the logic for each into a function

p-himik14:04:57

Ah, right, I see what you're getting at. What are the benefits of such an approach?

emccue14:04:47

(sorry, going into multi-staged rant mode, i'll loop back)

emccue15:04:18

(defn handler:event-a
  [{:keys [db]} [_ data]]
  {:db (update-a db data)
   :dispatch-n [[:event-b data] [:event-c data]]}

(rf/reg-fx 
  :event-a
  handler:event-a)

(defn handler:event-b
  [db [_ data]]
  (update-b db data))

(rf/reg-event-db
  :event-b
  handler:event-b)

(defn handler:event-c
  [{:keys [db]} [_ data]]
  {:db (update-c db data)
   :http-xhrio { ... }})

(rf/reg-event-fx
  :event-c
  handler:event-c)

emccue15:04:46

and once they are in functions, it is convenient to always use reg-event-fx so all handler:thing functions have the same return type

emccue15:04:21

(defn handler:event-a
  [{:keys [db]} [_ data]]
  {:db (update-a db data)
   :dispatch-n [[:event-b data] [:event-c data]]}

(rf/reg-fx 
  :event-a
  handler:event-a)

(defn handler:event-b
  [{:keys [db]} [_ data]]
  {:db (update-b db data)})

(rf/reg-event-fx
  :event-b
  handler:event-b)

(defn handler:event-c
  [{:keys [db]} [_ data]]
  {:db (update-c db data)
   :http-xhrio { ... }})

(rf/reg-event-fx
  :event-c
  handler:event-c)

emccue15:04:42

and now we move all the keys other than :db into an :fx vector

emccue15:04:35

(defn handler:event-a
  [{:keys [db]} [_ data]]
  {:db (update-a db data)
   :fx [[:dispatch [:event-b data]]
        [:dispatch [:event-c data]]]}

(rf/reg-fx 
  :event-a
  handler:event-a)

(defn handler:event-b
  [{:keys [db]} [_ data]]
  {:db (update-b db data)})

(rf/reg-event-fx
  :event-b
  handler:event-b)

(defn handler:event-c
  [{:keys [db]} [_ data]]
  {:db (update-c db data)
   :fx [[:http-xhrio { ... }]]})

(rf/reg-event-fx
  :event-c
  handler:event-c)

emccue15:04:24

then we start to flatten - what is appropriate to do for this is highly dependent on what specifically is happening, but in general

emccue15:04:12

(defn handler:event-a
  [{:keys [db]} [_ data]]
  {:db (-> db
           (update-a data)
           (update-b data)
   :fx [[:dispatch [:event-c data]]]}

(rf/reg-fx 
  :event-a
  handler:event-a)

(defn handler:event-b
  [{:keys [db]} [_ data]]
  {:db (update-b db data)})

(rf/reg-event-fx
  :event-b
  handler:event-b)

(defn handler:event-c
  [{:keys [db]} [_ data]]
  {:db (update-c db data)
   :fx [[:http-xhrio { ... }]]})

(rf/reg-event-fx
  :event-c
  handler:event-c)

emccue15:04:41

start by inlining what you can simply - move common state updates into their own fns if you need to

p-himik15:04:10

Wait a second.

p-himik15:04:29

You're explaining to me every minute detail, but as I mention - I see where you're getting at. After all, I have participated in a previous discussion about it as well. But I still don't see what the tangible benefits of such an approach are. Easier to write some unit tests, maybe?

emccue15:04:23

easier to write unit tests and do debug in an inspector

p-himik15:04:56

Thanks! TBH I'm still not sold on the approach because it definitely has some downsides, but I'll take a deeper look at it once I have issues with debugging and testing.

emccue15:04:22

like you can do

(t/is (some (fn [[effect-key args]]
                (and (= effect-key :http-xhrio)
                     (= "some.domain" (get args :uri))))
            (:fx (handler:event-a {} [...]))))

p-himik15:04:05

Yeah, I get that.

emccue15:04:07

also remember that dispatches are async - we've had so many dumb issues because rending and state updates don't happen atomically with dispatches

p-himik15:04:02

A call to dispatch is indeed async. The :dispatch effect - not entirely. IIRC the next render won't happen until the even queue is empty. That's why the :flush-dom metadata has been introduced.

emccue14:04:28

so manipulating srcObject would be an "effect", and we would put it in the :fx returned from the event that wanted to perform that side effect

lilactown14:04:02

I would not control the UI from a re-frame effect

lilactown14:04:12

you are going to experience pain by doing this

emccue14:04:35

(^you experience pain doing any imperative ref stuff in a "monad-ey" functional system, but I digress)

lilactown14:04:18

yeah re-frame events/effects are just not built for handling UI stuff

lilactown14:04:27

manipulating the UI should be handled by react/reagent. re-frame should handle business logic and other "headless" concerns

👍 3
lilactown14:04:09

the issue being that events and effects are decoupled from the UI lifecycle, so you can easily have events and fx in the queue that are no longer valid. e.g. the user clicks the "stop" button to control a video, then navigates away from the screen with the video, unmounting it. there isn't a way to cancel that effect which is going to try and read the DOM and mutate it

lilactown14:04:58

that's why re-frame always suggests using subscriptions, but you run into the second problem: re-frame events and effects are handled in an async queue, so if you are controlling an input with a subscription then you'll end up with a latency between user input and updating the state and re-rendering

emccue14:04:03

(all the same caveats as Elm's ports, but they don't have the option of giving up so if you want advice you can search through what those people do - its all very possible even with the issues pointed out above)

lilactown14:04:36

what are you trying to say, emccue?

emccue14:04:53

that you are right about the issues

emccue14:04:59

but it is also still doable

lilactown15:04:14

I agree it is doable, but I wouldn't 😛 that's my advice

p-himik15:04:51

For some complex components, switching that particular component completely to Reagent might be not worth it. E.g. a component that requires some data that has to be requested from somewhere but could already be cached in app-db and that ends up being used in an imperative way due to how the DOM API works. Putting all of that through the interface of that particular component will create quite a monster.

lilactown15:04:54

yes but given the decision between me handling that complexity in the code vs my users having to deal with a poor experience, I am forced to handle the complexity

p-himik15:04:39

I believe emccue meant something other than "poor experience" by "it is also still doable". :)

p-himik15:04:00

Depends on a particular use-case of course, but if e.g. a particular DOM element might disappear before some particular effects mutates it, you can simply check in the effect handler that the element is still there. There are more complex issues, of course, and many of them, if not all, can be handled by using dispatch-sync.

kennytilton16:04:51

So the use case seems terribly common: a user gesture says "let's edit this softball team, the Tigers". A modal (we prefer) needs to show itself and the team. Duh. So when the user makes the gesture we have to load the team data and trigger the modal to open. That is how the view function ended up dispatching its own "load team" event. But that is away from re-frame goodness. And it does not even work: the http-get to load the team takes too long, and view gets built before the data is there. So how does everyone do this? Right now I am thinking dispatch-sync, https://github.com/day8/re-frame-async-flow-fx, or writing a custom http-get chaining to an app.db update of "team-to-edit" in the app DB and having the modal subscribe just to that as a two-fer data payload and show/hide flag for the modal. The latter would be a Poor Man's dispatch-sync I guess. Sound OK? Comments/shrieks of horror welcome. 🙏

p-himik16:04:30

(require '[re-frame :as rf])

(rf/reg-event-fx
  ::open-modal
  (fn [{db :db} _]
    {:db         (assoc db :modal {:visible?     true
                                   :in-progress? true})
     :http-xhrio {...
                  :on-success [::-store-modal-data]}}))

(rf/reg-event-db
  ::-store-modal-data
  (fn [db [_ data]]
    (update db :modal assoc :in-progress? false :data data)))

(rf/reg-sub
  ::modal-visible?
  (fn [db _]
    (-> db :modal :visible?)))

(rf/reg-sub
  ::modal-in-progress?
  (fn [db _]
    (-> db :modal :in-progress?)))

(rf/reg-sub
  ::modal-data
  (fn [db _]
    (-> db :modal :data)))

(defn page []
  [:div
   [:button {:on-click #(rf/dispatch [::open-modal])}
    "Show modal"]
   [modal]])

(defn modal []
  [:div {:style (when-not @(rf/subscribe [::modal-visible?]) {:display :none})}
   (if @(rf/subscribe [::modal-in-progress?])
     [progress-indicator]
     [modal-data-panel @(subscribe [::modal-data])])])

emccue16:04:16

(rf/reg-event-fx
  ::user-gestured-to-open-modal
  [{:keys [db]} [_ team-id]]
  {:db (assoc db ::modal-state
              {:team-data {:status :loading}
               :open      true})
   :fx [[:http-xhrio {:uri (str "/get/team/" team-id)
                      :on-success [::successfully-loaded-team-data-for-modal]
                      :on-failure [::failed-to-load-team-data-for-modal]}]]})

(rf/reg-event-fx
  ::successfully-loaded-team-data-for-modal
  [{:keys [db]} [_ team-data]]
  {:db (update db ::modal-state
               assoc :team-data {:status :success
                                 :data   team-data}})

(rf/reg-event-fx
  ::failed-to-load-team-data-for-modal
  [{:keys [db]} [_ error]]
  {:db (update db ::modal-state
               assoc :team-data {:status :failure
                                 :error  error}})

(rf/reg-event-fx
  ::user-gestured-to-close-modal
  [{:keys [db]} _]
  {:db (update db ::modal-state 
               assoc :open false)})
  

kennytilton22:04:25

I am humbled, @U2FRKM4TW and @U3JH98J4R. Thanks so much for these. I am totally going with a solution in which the modal looks for :team-editor-data or some such, and chained dispatch sees to it that the modal gets one complete delivery when everything is ready for the modal. And it turns out I missed the "new team" use case, in which no http-get of any existing team will be needed. So there will just be a single payload indicating "create" or "edit", and various paths will end by adding that to app db, just as your examples suggest. Bravo #re-frame!

jkrasnay16:04:12

I just put a condition in my modal that checks if the data from the GET has arrived yet. If not, I display a little spinner component. When the data arrives the body of the modal will be re-rendered.

emccue17:04:53

I feel like I need to carry soap boxes around reminding people to handle encoding failure and not-asked states in their model

kennytilton22:04:24

Great idea. That would avoid our situation where our own wrapper tries to launch without the data, then our wrapper gets confused because it did not anticipate this use case. I actually sorted things out before noticing the "new thing" use case, in which no data is expected. So I have to distinguish those two with a payload that includes :create-or-modify indicator in some form. Thx for the simple solution! 🙏

Rebecca Bruehlman13:04:58

looping back around on this …. So, if I shouldn’t manipulate srcObject with reagent and should instead let reframe handle setting the video stream, then…. I rewrote what I had to use subscriptions instead:

(defn video-stream [stream]
    [:div {:style {:overflow "hidden"
                   :max-width 500
                   :max-height 500
                   :width "100%"}}
     [:video
      {:auto-play true
       :srcObject stream
       :style {:max-width (:width 500)
               :max-height (:height 500)
               :width "80%"
               :height "80%"
               :position "relative"
               :left "50%"
               :top "10%"
               :transform "translate(-50%, 10%)"
               :object-position "center top"}}]])

(defn start-stream []
  (rf/dispatch [::vc-events/set-video-stream :local-stream]))

(defn video-chat-app []
  (let [stream @(rf/subscribe [::vc-subs/stream :local-stream])]
  
  (if (nil? stream)
    (start-stream)
    (video-stream stream))))
(defn get-stream []
  (-> js/navigator
      .-mediaDevices
      (.getUserMedia #js {:video true :audio true})))

(rf/reg-event-db
 ::set-video-stream
 (fn [db [_ type]]
   (vc-q/set-stream db type (get-stream))
))
However, because getUserMedia returns a promise, setting my component’s srcObject property literally sets said property to a promise that does not resolve (whereas setting it with (set! (.-srcObject element) stream) resolves said promise). So I must still be doing something wrong 😉 What is the right way to trigger displaying streaming video with reagent, if the proper way to go about things is to use subscriptions?