re-frame

https://github.com/Day8/re-frame/blob/master/docs/README.md https://github.com/Day8/re-frame/blob/master/docs/External-Resources.md
Rebecca Bruehlman 2021-04-22T00:47:23.266Z

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)?

Rebecca Bruehlman 2021-04-22T13:58:12.273700Z

@lasse.maatta 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 Bruehlman 2021-04-22T14:18:52.273900Z

@jacek.schae 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)…

lassemaatta 2021-04-22T14:41:53.275100Z

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 Bruehlman 2021-04-22T14:52:51.287600Z

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

jacekschae 2021-04-22T15:11:13.302700Z

@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 Bruehlman 2021-04-22T15:23:42.308800Z

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?

jacekschae 2021-04-22T15:47:19.310Z

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 Bruehlman 2021-04-22T00:48:20.267100Z

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?

lilactown 2021-04-22T14:45:17.278700Z

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

lilactown 2021-04-22T14:46:02.279700Z

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

p-himik 2021-04-26T13:37:48.340200Z

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-himik 2021-04-26T13:39:12.340400Z

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-himik 2021-04-26T13:44:39.340800Z

> 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.

Stuart Campbell 2021-04-22T02:04:39.267900Z

lol, I don't know why I didn't just try that. Thank you 🙂

lassemaatta 2021-04-22T03:58:17.268800Z

Have you read https://github.com/day8/re-frame/blob/master/docs/Effects.md ?

lassemaatta 2021-04-22T04:00:40.272400Z

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.

jacekschae 2021-04-22T04:28:19.272600Z

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

Lu 2021-04-22T09:24:19.273Z

How does a global interceptor address the issue? Say there are a total of 15 events and the get request needs to be dispatched from 10 of them. Would you have some conditions in the global interceptor to know whether the event in question is one of those 10 so you can make the get call?

p-himik 2021-04-22T09:28:01.273200Z

Shift your perspective towards views. There's a view that needs some data. The view is displayed only after some subscription returns true. So, you simply want to load that data as soon as the value of that subs is true. For that, you create a global interceptor and run the sub function there, comparing the values between the old and the new app-db values. Of course, ideally the sub function should just use get-in or something just as simple, since the global interceptor will be run on every event. So, when the value changes from false to true, the interceptor just dispatches an event that loads the data. That's it.

1👍
Lu 2021-04-22T09:29:21.273400Z

Oh yeah! I got it I was thinking the side effect needed to happen in the interceptor that’s why I was confused 🙂

Rebecca Bruehlman 2021-04-22T13:58:12.273700Z

@lasse.maatta 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 Bruehlman 2021-04-22T14:18:52.273900Z

@jacek.schae 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)…

emccue 2021-04-22T14:41:40.274600Z

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

lassemaatta 2021-04-22T14:41:53.275100Z

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.

emccue 2021-04-22T14:42:33.276Z

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

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

emccue 2021-04-22T14:43:08.276400Z

and it doesn't perform side effects

emccue 2021-04-22T14:44:00.277500Z

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

emccue 2021-04-22T14:44:24.278Z

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

emccue 2021-04-22T14:45:11.278600Z

which you should need fairly few of for your whole app

lilactown 2021-04-22T14:45:17.278700Z

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

emccue 2021-04-22T14:45:26.279100Z

and we also are moving to never using dispatch

lilactown 2021-04-22T14:46:02.279700Z

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

emccue 2021-04-22T14:46:28.280200Z

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

lilactown 2021-04-22T14:47:02.280600Z

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

lilactown 2021-04-22T14:47:12.280900Z

you are going to experience pain by doing this

p-himik 2021-04-22T14:47:29.281200Z

What are you using instead?

emccue 2021-04-22T14:47:42.281500Z

just flattening events

emccue 2021-04-22T14:48:04.281700Z

so like, lets say you had this

emccue 2021-04-22T14:48:35.282700Z

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

lilactown 2021-04-22T14:49:18.283500Z

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

lilactown 2021-04-22T14:49:27.283800Z

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

1👍
emccue 2021-04-22T14:52:00.286300Z

(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 { ... }}))

emccue 2021-04-22T14:52:08.286600Z

this kinda work as an example?

lilactown 2021-04-22T14:52:09.286800Z

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

Rebecca Bruehlman 2021-04-22T14:52:51.287600Z

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

p-himik 2021-04-22T14:54:03.289200Z

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?

emccue 2021-04-22T14:54:18.289700Z

no this is the counter example

emccue 2021-04-22T14:54:34.290Z

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

emccue 2021-04-22T14:54:50.290500Z

just making sure we are at the same starting point

lilactown 2021-04-22T14:54:58.291100Z

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

emccue 2021-04-22T14:55:03.291200Z

(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)

emccue 2021-04-22T14:57:45.291600Z

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

emccue 2021-04-22T14:57:59.291800Z

we start by moving the logic for each into a function

lilactown 2021-04-22T14:58:36.292700Z

what are you trying to say, emccue?

emccue 2021-04-22T14:58:53.293300Z

that you are right about the issues

p-himik 2021-04-22T14:58:57.293600Z

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

emccue 2021-04-22T14:58:59.293800Z

but it is also still doable

lilactown 2021-04-22T14:59:16.294100Z

ok

emccue 2021-04-22T14:59:47.294600Z

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

emccue 2021-04-22T15:02:18.296600Z

(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)

emccue 2021-04-22T15:02:46.297200Z

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

emccue 2021-04-22T15:03:21.297900Z

(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)

emccue 2021-04-22T15:03:42.298200Z

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

emccue 2021-04-22T15:04:35.298400Z

(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)

lilactown 2021-04-22T15:05:14.298900Z

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

emccue 2021-04-22T15:06:24.299300Z

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

emccue 2021-04-22T15:07:12.300400Z

(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)

emccue 2021-04-22T15:07:41.301100Z

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

p-himik 2021-04-22T15:07:51.301500Z

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.

p-himik 2021-04-22T15:08:10.301600Z

Wait a second.

p-himik 2021-04-22T15:09:29.302Z

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?

emccue 2021-04-22T15:10:23.302400Z

easier to write unit tests and do debug in an inspector

jacekschae 2021-04-22T15:11:13.302700Z

@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

p-himik 2021-04-22T15:11:56.302900Z

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.

emccue 2021-04-22T15:13:22.303100Z

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-himik 2021-04-22T15:14:05.303300Z

Yeah, I get that.

emccue 2021-04-22T15:15:07.303500Z

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-himik 2021-04-22T15:17:02.304Z

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.

lilactown 2021-04-22T15:17:54.304900Z

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-himik 2021-04-22T15:18:12.305200Z

https://github.com/day8/re-frame/issues/480

p-himik 2021-04-22T15:20:39.306800Z

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

p-himik 2021-04-22T15:22:00.308500Z

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.

Rebecca Bruehlman 2021-04-22T15:23:42.308800Z

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?

lilactown 2021-04-22T15:23:59.309100Z

ok

jacekschae 2021-04-22T15:47:19.310Z

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

kennytilton 2021-04-22T16:13:51.319300Z

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. 🙏

kennytilton 2021-04-23T22:20:25.328800Z

I am humbled, @p-himik and @emccue. 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!

2021-04-22T16:18:12.320500Z

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.

kennytilton 2021-04-23T22:10:24.328600Z

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! 🙏

p-himik 2021-04-22T16:22:30.320600Z

(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])])])

emccue 2021-04-22T16:35:16.320800Z

(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)})
  

emccue 2021-04-22T17:12:53.321200Z

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