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)?
@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?)@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)âŚ
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.
correct, sorry. My Python is coming through with the verbiage đ I know the app db is entirely replaced
@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 sequentialRight, 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?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
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?
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
I would keep any manipulation of the UI in reagent/react and let re-frame handle business logic and global app side effects
You would have to call .then
on that promise, and in the passed function call dispatch
to store the actual stream in the DB.
Also, you should rewrite your video-chat-app
component:
- 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
> 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.
Have you read https://github.com/day8/re-frame/blob/master/docs/Effects.md ?
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.
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@rbruehlman For me, reg-event-fx always returns :db and :fx
(defn handler:some-event
[{:keys [db]} _]
{:db (... update state ...)
:fx [ (... effects in order ...) (...) (...) ]})
(rf/reg-event-fx
event:some-event
handler:some-event)
and it doesn't perform side effects
so you kick off any "side effects" you want to perform as a result of the call directly
those "side effects" are ultimately supplied by reg-fx
which you should need fairly few of for your whole app
and we also are moving to never using dispatch
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
I would not control the UI from a re-frame effect
you are going to experience pain by doing this
What are you using instead?
just flattening events
so like, lets say you had this
(^you experience pain doing any imperative ref stuff in a "monad-ey" functional system, but I digress)
yeah re-frame events/effects are just not built for handling UI stuff
manipulating the UI should be handled by react/reagent. re-frame should handle business logic and other "headless" concerns
(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 { ... }}))
this kinda work as an example?
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
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?
no this is the counter example
i'm just starting with this to show how we transform it
just making sure we are at the same starting point
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
(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)
so if you assume all of event-a, event-b, and event-c are being dispatched somewhere in the code
we start by moving the logic for each into a function
what are you trying to say, emccue?
that you are right about the issues
Ah, right, I see what you're getting at. What are the benefits of such an approach?
but it is also still doable
ok
(sorry, going into multi-staged rant mode, i'll loop back)
(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)
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
(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)
and now we move all the keys other than :db into an :fx vector
(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)
I agree it is doable, but I wouldn't đ that's my advice
then we start to flatten - what is appropriate to do for this is highly dependent on what specifically is happening, but in general
(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)
start by inlining what you can simply - move common state updates into their own fns if you need to
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.
Wait a second.
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?
easier to write unit tests and do debug in an inspector
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.
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 {} [...]))))
Yeah, I get that.
also remember that dispatches are async - we've had so many dumb issues because rending and state updates don't happen atomically with dispatches
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.
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
I believe emccue meant something other than "poor experience" by "it is also still doable". :)
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
.
ok
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. đ
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!
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.
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! đ
(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])])])
(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)})
I feel like I need to carry soap boxes around reminding people to handle encoding failure and not-asked states in their model