re-frame

Luke Johnson 2024-08-22T15:44:38.599579Z

Hello. I have a simple question that has probably been answered before but I can’t find it in docs or online. I want to toggle a :loading? boolean in my app-db while I’m making http calls. Should my event handlers update the db directly or dispatch an event to toggle it? What is best practice, more performant, or best user experience? What if the http response handlers may trigger other http calls? So loading would be true, false, true, false in quick succession.

Luke Johnson 2024-08-22T15:47:35.517949Z

To clarify, which is best practice in a rf/reg-event-fx? 🅰️

{:db (assoc db :loading? true)
 :http-xhrio (...)}
🅱️
{:fx [:dispatch [::loading? true]]
 :http-xhrio (...)}

p-himik 2024-08-22T15:49:03.765169Z

> Should my event handlers update the db directly Most likely, yes. > What if the http response handlers may trigger other http calls? Use a state machine.

Luke Johnson 2024-08-22T15:55:18.729729Z

Thanks for your quick response! I’ll continue updating the db directly, like 🅰️.

👍 1
2024-08-22T16:02:16.344619Z

Yes 🅰️ is the typical choice. If you did a deferred nested :dispatch that actually may not happen “soon enough” and your UI could have a flicker state or something of that form. When you start to get into “chained” async flow of this type of events happening, I like https://github.com/day8/re-frame-async-flow-fx We’ve used it extensively in some pretty large apps. However, we did fork it so that it had cljc support since that was convenient for some of our testing pipeline. Need to push a PR to the main repo to see if they’ll take that change at some point.

👍 1
Luke Johnson 2024-08-22T16:03:42.045689Z

Nice. For now we just have a single chain of 2 potential calls. But if that ever expands, I’ll be looking much more closely at async-flow-fx. Thanks!

2024-08-22T16:04:07.868039Z

We often have an async flow of events - perhaps doing XHR in the process. We keep track of all this and have a loading state to go with it. We use signals there to know when the whole operation is done.

p-himik 2024-08-22T16:04:40.863639Z

Note that it also common here to specifically recommend not using re-frame-async-flow-fx.

2024-08-22T16:04:47.582129Z

I didn’t use async flow originally for smaller cases. It started to get carried away without some proper infrastructure (like this lib) at a certain point.

2024-08-22T16:05:14.910849Z

I don’t know why you’d recommend against it. The idea at least is quite sound. The impl is fairly basic. Would be good to see the critiques.

2024-08-22T16:06:11.354099Z

We’ve certainly used it in very complex flows in some large apps that have quite a few devs involved over time (so coordination is involved too). We also are sensitive to performance etc.

p-himik 2024-08-22T16:07:16.716049Z

> Would be good to see the critiques. Just search in this channel. ;)

2024-08-22T16:09:48.679289Z

I did. I’ve mostly just read “there have been problems” with no clear rationale. Then there was this older one https://clojurians.slack.com/archives/C073DKH9P/p1631039809068800?thread_ts=1631015086.050100&cid=C073DKH9P but again, it just says “it is bad” and doesn’t say why.

2024-08-22T16:10:05.646159Z

I wouldn’t recommend using it where it isn’t needed. But that tautological anyways.

2024-08-22T16:10:44.663569Z

If you have to chain async events together into one coherent piece though - you really need some sort of state machine implemented one way or another. Doing it manually with a bunch of coupled event handlers is a huge pain.

2024-08-22T16:11:34.480269Z

But I have done it other ways before. async-flow just really simplified that logic. If another lib wanted to do it an alt way, that’d be interesting - but haven’t seen anything of note.

2024-08-22T16:20:49.113279Z

I also found https://gist.github.com/olivergeorge/edc572eab653cc228ec5ecbbcc8271a2 which is the most concrete thing I found. I don’t see how it addresses dependencies among async actions though. Commented there as such. Either way, I remain unconvinced here. I think async-flow can be abused, but so can anything else.

p-himik 2024-08-22T16:23:38.245259Z

Hmm, it might be harder to find than I thought. Could easily find only this: https://clojurians.slack.com/archives/C073DKH9P/p1644175506330019?thread_ts=1643901164.589709&cid=C073DKH9P And that second item kinda hints at a bigger picture given that events themselves are async by nature. When you have stuff like {:when :seen? :events [...]}, all of those events must be used with the async flow in mind. You can't reuse them in other contexts at all. With that in mind, it becomes trivial to implement race conditions. It becomes trivial for you or someone else to trip over your code just because it implicitly monitors an event that's now used elsewhere. Apart from that, it's not composable. If you have an existing workflow and then decide that it should be a part of another workflow, you aren't gonna have a good time.

2024-08-22T16:24:49.088819Z

We have done this in large complex apps with quite a few devs - even new and old cycling in and out. I think it can be quite scalable. You do have to have some level of convention to avoid pitfalls. All of this gets complicated either way in large apps.

2024-08-22T16:25:02.687729Z

We know the events that are for flows. You could even organize it via conventions in a way that makes it clear.

p-himik 2024-08-22T16:26:25.361209Z

I don't see any points that would make re-frame-async-flow-fx better than a state machine. > We have done this in large complex apps with quite a few devs It doesn't mean that your experience is generalizable. JavaEE has worked for many people as well. Or C++, or whatever - it doesn't mean that if somebody could do it with C++ then C++ should be recommended to anyone who wants to do a similar thing.

2024-08-22T16:26:28.834859Z

I think you can easily get into race conditions and a mess doing it with no structured support too. I think its worse even when ad hoc. We have apps to compare X vs Y with this stuff really. Our older apps didn’t use async-flow, then we tried to retrofit a few to use it and use it in newer apps. We get far more problems in our apps without it and makes us want to spend time to refactor to use async-flow there too.

2024-08-22T16:27:06.338119Z

I think we’ve had a lot of success with it is my point. I see pretty weak theoretical arguments that it “could be bad”. So I don’t think general community discouragement makes sense.

2024-08-22T16:27:38.539009Z

I think the JavaEE analogy is not really related either. async-flow is a tiny library to implement a small state machine.

p-himik 2024-08-22T16:27:44.736379Z

They are not theoretical. :) Perhaps @emccue could chime in.

2024-08-22T16:28:00.066009Z

If I were to implement it from scratch, I may do it slightly differently on implementation choices - but conceptually it works quite nice.

emccue 2024-08-22T16:28:37.262699Z

Oh great

2024-08-22T16:28:46.521189Z

If you do just end up implementing a state machine ad hoc locally, that’s fine too. But seems like you’d then have to copy/paste that around or pull out some lib for it if you had multi apps. Also docs etc.

emccue 2024-08-22T16:29:02.391999Z

yeah I had an extremely bad experience there

emccue 2024-08-22T16:29:09.840109Z

whatever clarification you want I can give

2024-08-22T16:29:28.780239Z

It’s just vague. I’ve had a very good experience. And with quite a bit of scale.

p-himik 2024-08-22T16:29:32.022549Z

> async-flow is a tiny library to implement a small state machine. But it's not. It's not a state machine at all. It doesn't have states. If anything, it's like a reactive flow but imposed externally.

emccue 2024-08-22T16:29:45.390849Z

the codebase where i had issues is also 100% dead now, so i can probably get away with sharing direct snippets

p-himik 2024-08-22T16:30:03.806339Z

> If you do just end up implementing a state machine ad hoc locally, that’s fine too There are also libraries for this, including re-frame-aware ones.

emccue 2024-08-22T16:30:54.545469Z

Let me introduce the audience to our emblematic bastard

emccue 2024-08-22T16:30:56.514989Z

(rf/reg-event-fx
  ::init-tracking
  (fn [cofx _]
    {:dispatch [::db-util/xhr-with-loading ::load-tracking-data
                {:method     :get
                 :uri        "/api/tracking-info"
                 :on-success [::process-tracking]}]}))

2024-08-22T16:31:04.824109Z

It’s more of an async DAG - but this is what the typical problem is. Quite similar to core.async or JS await/async stuff etc.

emccue 2024-08-22T16:31:31.712599Z

(rf/reg-event-fx
  ::init
  (fn [cofx [_ initial-db have-auth have-creator root]]
    {:db (-> (merge (:db cofx) initial-db)
             (init-db-loading-status)
             (bootup/set-app-context bootup/app-context:advertiser))
     :async-flow
     {:id             ::lumanuapp-async
      :db-path        [::lumanuapp-async]
      ;; APP BOOT UP SEQUENCE
      ;; 1. Trigger the first rules:
      :first-dispatch [(if have-auth ::begin-booting-auth ::begin-booting)]
      :rules          [{:when       :seen-any-of?
                        :events     [::begin-booting ::begin-booting-auth]
                        :dispatch-n [[::init-tracking]
                                     [::bootup/render-app root]
                                     [::link-helpers/register-non-app-link-click-listener]]}
                       ;; END APP BOOTUP SEQUENCE

                       ;; MAYBE.
                       ;; Initial data loading rules
                       {:when       :seen-all-of?
                        :id         :base-rule ; undocumented feature of async-flow
                        :events     [(fn [& args]
                                       (and (apply (supported-for-user-type-matcher have-creator) args)
                                            (apply (routing/legacy-advertiser-route-changed-matcher smartboost-routes) args)))]
                        :dispatch-n [[::advertiser/fetch-team-creators]
                                     [::smartboost/fetch-smartboost-channels]
                                     [::sb-settings/fetch-brands]]}
                       {:when     :seen?
                        :events   ::advertiser/completed-fetching-creators
                        :dispatch [::smartboost/creators-loading-complete]}
                       {:when       :seen-all-of?
                        :events     [(fn [& args]
                                       (and (apply (supported-for-user-type-matcher have-creator) args)
                                            (apply (routing/legacy-advertiser-route-changed-matcher collab-routes) args)))]
                        :dispatch-n [[::collabs/fetch-collaborations]
                                     [::collabs/fetch-collaborating-advertising-links]
                                     [::collabs/fetch-collab-participants]
                                     [::advertiser/fetch-team-creators]
                                     [::collabs/fetch-terms]
                                     [::collabs/completed-fetching-collabs]]}

                       {:when       :seen-all-of?
                        :events     [(fn [& args]
                                       (and (apply (supported-for-user-type-matcher have-creator) args)
                                            (apply (routing/legacy-advertiser-route-changed-matcher payment-routes) args)))]
                        :dispatch-n [[::payments/fetch-all-payments-info]
                                     [::advertiser/fetch-team-creators]
                                     (bulk-payments-events/init)
                                     (bulk-pay-det-events/init)]}

                       {:when     :seen-all-of?
                        :events   [::channel-db/set-creator-id->advertising-links
                                   ::advertiser/completed-fetching-creators
                                   ::collabs/completed-fetching-collabs]
                        :dispatch [::collabs/set-collabs-loading-completed]}]}}))

emccue 2024-08-22T16:32:55.053159Z

This was the async-flow init event that started up our app

emccue 2024-08-22T16:32:59.328709Z

the root problems we had were

2024-08-22T16:34:05.417069Z

I really don’t mind the above flow. I’d say we do not try to make really generic event handlers as sort of utilities for other things though. Like this ::db-util/xhr-with-loading - we’d just set loading statuses in a specific area of our app-db and have utils around updates to that.

2024-08-22T16:34:45.057809Z

Basically, we don’t do “higher order event handlers” - just helpers that can update the cofx/fx map directly for reused patterns.

emccue 2024-08-22T16:35:18.798339Z

• This gives zero shits if any of the events "fail", just that they happened • Everything only runs once. To load all the data you need "for a page" you need to hack in and reset/refire some of this nonsense • It gets huge. This is extremely trimmed down from what it was • It is very difficult to reason what state the app is in because events work exactly opposite to regular functions

2024-08-22T16:35:20.781549Z

Happy to hear your details though. It’s nice to see concrete stuff in general (most people can’t share things really).

2024-08-22T16:35:50.938899Z

We never end up with flows as big as above. So not sure what the disconnect is there.

emccue 2024-08-22T16:35:53.096899Z

> Basically, we don’t do “higher order event handlers” - just helpers that can update the cofx/fx map directly for reused patterns. And that is the problem, essentially

2024-08-22T16:35:58.155869Z

We always handle failure cases.

emccue 2024-08-22T16:36:49.595989Z

If you have events lead directly dispatch other events, which is both extremely tempting and lightly encouraged by the official docs, it becomes very hard to reason about things

2024-08-22T16:37:07.684539Z

I’d really like to see an example of how you manage a DAG of async events in other ways really. It’s not an easy thing to reason about conceptually or debug in general as it gets complex.

emccue 2024-08-22T16:37:13.066699Z

and async-flow-fx encourages that + adds a very brittle caching step on top of it

2024-08-22T16:37:22.943629Z

> If you have events lead directly dispatch other events, which is both extremely tempting and lightly encouraged by the official docs, it becomes very hard to reason about things I don’t see how this is really even practically avoidable.

2024-08-22T16:37:39.297059Z

You have to sometimes have dependencies where things must “wait” for other stuff to be done.

emccue 2024-08-22T16:37:57.095269Z

well think about it, when you are in an event you have the db and you have the ability to put any number of effects

emccue 2024-08-22T16:38:15.966349Z

So there is never a mechanical reason to use an event as a helper

2024-08-22T16:38:35.634709Z

Yeah, you could role your own continuation etc. if that is what is being suggested

emccue 2024-08-22T16:38:47.401369Z

no

emccue 2024-08-22T16:38:49.328759Z

just

emccue 2024-08-22T16:38:56.118019Z

sorry i got a lot to type for this to make sense

emccue 2024-08-22T16:39:02.472299Z

hold with me

2024-08-22T16:39:14.835959Z

I agree it doesn’t need to be event dispatch specifically. Something like a callback continuation could also work. It’s basically just CSP stuff.

emccue 2024-08-22T16:39:23.379289Z

no, not that either

2024-08-22T16:39:31.794629Z

Ok, sorry I’ll just hold back for a bit to let you “cook” 😉

emccue 2024-08-22T16:40:10.399719Z

lets go through a few scenarios

emccue 2024-08-22T16:40:24.114879Z

first, our app just tracks a number

emccue 2024-08-22T16:40:29.784239Z

{:n 0}

emccue 2024-08-22T16:40:32.410999Z

this is the app db

emccue 2024-08-22T16:40:57.709069Z

we have two buttons, one which should multiply by 3 and add 1, another which should divide by 2

emccue 2024-08-22T16:42:12.393309Z

(rf/reg-event-db
  :mul-3-add-1
  (fn [db _]
    (assoc db :n (inc (* 3 (:n db))))))

(rf/reg-event-db
  :divide-2
  (fn [db _]
    (assoc db :n (/ (:n db) 2))))

emccue 2024-08-22T16:42:28.223529Z

so far so good?

👍 2
emccue 2024-08-22T16:43:03.957179Z

okay so now lets move adding one to its own event

p-himik 2024-08-22T16:43:41.744319Z

> It’s more of an async DAG With every node being a start node, yes. Saying "but we're very careful, so it's not a problem" is not a fix to this problem. The analogy with C++ still stands. I used to know how to write C++ correctly and effectively, a decade ago. And after realizing how much thought has to go into it to make sure that it all works properly, I would never recommend C++ to anyone. If a thing makes it way, way too easy to trip up people when alternatives that are strictly better exist, that thing is not a good thing. > Quite similar to core.async or JS await/async stuff etc. Both of those have a very explicit DAG imposed from within a system. You cannot tell JS to run some function X whenever a function Y is being run, unless you deliberately monkey-patch the hell out of the function Y.

emccue 2024-08-22T16:44:17.921139Z

(rf/reg-event-fx
  :mul-3
  (fn [db _]
    {:db (assoc db :n (* 3 (:n db)))
     :dispatch [:add-1]}))

(rf/reg-event-db
  :add-1
  (fn [db _]
    (assoc db :n (inc (* 3 (:n db))))))

(rf/reg-event-db
  :divide-2
  (fn [db _]
    (assoc db :n (/ (:n db) 2))))

emccue 2024-08-22T16:44:40.185819Z

pausing here: give some initial thoughts

2024-08-22T16:45:05.176639Z

I figure your are going to now try to put this in a particular order and want to ensure that order. Other than that, that’s my only current thought. I think perhaps that :add-1 is wrong its impl too in that regard - shouild just be (inc (:n db)) if I understand the intent.

2024-08-22T16:46:21.880939Z

With every node being a start node, yes.Doesn’t make sense to me. > Saying “but we’re very careful, so it’s not a problem” is not a fix to this problem. I think conventions are a very important thing within a re-frame app. It’s very open ended and can quickly become chaotic. The same is true for basically any web framework. You have to care about organization. > If a thing makes it way, way too easy to trip up people when alternatives that are strictly better exist, that thing is not a good thing. I’ve yet to see the alternatives that are “strictly better”. This is a key problem I have with the counter argument here. > Both of those have a very explicit DAG imposed from within a system. > You cannot tell JS to run some function X whenever a function Y is being run, unless you deliberately monkey-patch the hell out of the function Y. Doing the “bad re-frame” thing is the same as the “bad JS” thing that is alluded to here as absurd and no one would/should do it.

emccue 2024-08-22T16:48:52.670679Z

So i'm pretty sure re-frame will make the dispatch here run before any other events are run, so as-is there are no particular issues. Just a strange way to break up the code. But pay attention to there being no mechanical impediment to just running the logic to update the db in that first event.

emccue 2024-08-22T16:49:16.841139Z

add another wrinkle, lets log out the db

emccue 2024-08-22T16:50:14.171199Z

(rf/reg-event-fx
  :mul-3
  (fn [{:keys [db]} _]
    {:db (assoc db :n (* 3 (:n db)))
     :dispatch-n [[:log-db] [:add-1]]}))

(rf/reg-event-db
  :log-db
  (fn [db _]
    (println db)
    db)

(rf/reg-event-db
  :add-1
  (fn [db _]
    (assoc db :n (inc (* 3 (:n db))))))

(rf/reg-event-db
  :divide-2
  (fn [db _]
    (assoc db :n (/ (:n db) 2))))

p-himik 2024-08-22T16:50:27.731489Z

> Doesn’t make sense to me. The events in :seen are being monitored globally. Not just within a context of a workflow. If an event is used elsewhere while the workflow is running (between :setup and :halt? true), the :dispatch parts will also be triggered. It's trivial to have a fragmented workflow that you have no way of easily debugging. Even with re-frame-10x, you can see what event resulted in which effects but you cannot see from an event why it has been dispatched. > I’ve yet to see the alternatives that are “strictly better” Have you tried e.g. this? https://lucywang000.github.io/clj-statecharts/docs/integration/re-frame/

2024-08-22T16:50:33.247369Z

I think the example would be better to see if it wasn’t as easy to “just update the db” - like if an HTTP req had to be made to the server to do each math op. Perhaps you are just having an assumption that it is like that here?

emccue 2024-08-22T16:50:33.511689Z

so pop quiz - which of these will observe the db in a consistent state

emccue 2024-08-22T16:50:51.261919Z

@mikerod 🧑‍🍳

emccue 2024-08-22T16:51:08.329089Z

(rf/reg-event-fx
  :mul-3
  (fn [{:keys [db]} _]
    {:db (assoc db :n (* 3 (:n db)))
     :dispatch-n [[:log-db] [:add-1]]}))
or
(rf/reg-event-fx
  :mul-3
  (fn [{:keys [db]} _]
    {:db (assoc db :n (* 3 (:n db)))
     :dispatch-n [[:add-1] [:log-db]]}))

2024-08-22T16:52:37.190309Z

I don’t know what “consistent state” means here. Which one will log the state of db after the math ops have happened?

emccue 2024-08-22T16:52:51.222369Z

after both the *3 and the +1

2024-08-22T16:52:56.721159Z

Second

2024-08-22T16:53:24.947089Z

But you’d have to know that :add-1 is not going to need to do async steps to finish the op.

emccue 2024-08-22T16:53:27.980629Z

okay now lets change how log-db works

emccue 2024-08-22T16:54:21.308299Z

(rf/reg-event-db
  :info
  (fn [db _]
    (println db)
    db)

(rf/reg-event-fx
  :log-db
  (fn [{:keys [db]} _]
    {:db db
     :dispatch [:info]))

emccue 2024-08-22T16:54:56.862169Z

now which one will see both changes

emccue 2024-08-22T16:55:21.552419Z

wait

emccue 2024-08-22T16:55:50.063609Z

there

Luke Johnson 2024-08-22T16:56:49.782469Z

They both will?

emccue 2024-08-22T16:57:02.043639Z

try it

p-himik 2024-08-22T16:57:11.239699Z

@james.luke.johnson Sorry for hijacking the thread BTW.

👍 1
Luke Johnson 2024-08-22T16:57:26.002129Z

🙂 It’s okay, you answered my question immediately. The rest is just gravy.

😆 1
2024-08-22T16:57:42.545499Z

> The events in :seen are being monitored globally. Not just within a context of a workflow. > If an event is used elsewhere while the workflow is running (between :setup and :halt? true), the :dispatch parts will also be triggered. > It’s trivial to have a fragmented workflow that you have no way of easily debugging. Even with re-frame-10x, you can see what event resulted in which effects but you cannot see from an event why it has been dispatched. I am not sure I’m interpreting your concerns here correctly, but it sounds to me like “synthetic event” names in async-flow to trigger each step - we don’t refer to event handlers specifically. eg. :seen? :events [[::async-flow/notify ::something maybe-args-here]] We found decoupling the “notify” signals helped quite a bit. Perhaps that is what you are alluding to as a problem when not done. > Have you tried e.g. this? https://lucywang000.github.io/clj-statecharts/docs/integration/re-frame/ No, I will take a look. I hope it actually handles async flow dependencies though. A lot of examples seem to ignore that there can be ordered deps to deal with.

emccue 2024-08-22T16:57:42.791419Z

Oh i just got pinged. @james.luke.johnson I will probably be giving you a different answer by the end of this

emccue 2024-08-22T16:59:43.778039Z

sorry if my examples are sloppy, but i'm kinda making them up

emccue 2024-08-22T17:00:02.228299Z

say instead you wanted to observe the time between *3 and +1.

2024-08-22T17:00:11.542019Z

I don’t see how the example above could go wrong, but I’m also starting to not really see the purpose in this exercise. Even if this somehow goes out of order in a way I don’t see (in modern re-frame where :db always goes first). I’d never have just implicit assumptions like this. Event handlers know to wait for what they need to wait for. It’s actually a big reason async flow is so useful - it actually makes them wait in the correct DAG order.

emccue 2024-08-22T17:00:44.640089Z

Your event order will go from :mul-3 :log-db :add-one to :mul-3 :log-db :add-one :info

2024-08-22T17:00:55.370699Z

I think the example may be able to show a point - only maybe - better if all math ops were delayed as well. That at least shows how you can get lost in a tangle of async dispatches hanging out.

emccue 2024-08-22T17:01:09.073699Z

im building to the full set of problems

2024-08-22T17:01:14.150859Z

Ok, yes, I can see your interleaving example indeed.

2024-08-22T17:01:27.020509Z

We’d only ever assume you can’t expect an event to be “done” in a sequence though

2024-08-22T17:01:55.376509Z

As in if we wanted :log-db to happen before :add-one, we’d not rely on :dispatch-n and just assume dispatches have their effects fully done in that “cycle”. Again, this is actually where async-flow comes in. So sounds like a good demonstration for it’s usefulness! 😆

emccue 2024-08-22T17:02:22.381089Z

the first one here is just that when you turn what could otherwise be helper functions into dispatches you get potentially hard to understand behavior on refactors. Its not safe to turn an event into a chain of dispatches or to add more dispatches to any given event without understanding all the events that rely on it

emccue 2024-08-22T17:02:23.660349Z

no

emccue 2024-08-22T17:03:41.630989Z

so the clojure codebase started here. Then eventually someone came up with ::db-util/xhr-with-loading. It set some generic loading flag and made an http request over a chain of a few events

emccue 2024-08-22T17:04:18.472959Z

which was neat because then there was a one stop place for • Make http request • Get something in the db you can subscribe to for when that finishes

p-himik 2024-08-22T17:04:35.030729Z

> Perhaps that is what you are alluding to as a problem when not done. Not really. An example from the README: {:when :seen? :events :success-X :dispatch [:do-Y]}. Let's assume that :success-X is not just a "marker event" that can be handled with ::async-flow/notify but rather a proper event that does something, e.g.

(rf/reg-event-db :success-X
  (fn [db [_ success-key]]
    (update db :success-markers (fnil conj #{}) success-key)))
It's a very generic event, but also not generic enough to go against re-frame docs that warn against things like :get. However, if it's used in a :events vector in an async flow, it will be a fertile ground for a partial workflow where someone decides to use :success-X elsewhere, unknowingly triggering all the related :dispatch events. Those events might not even affect the current page that the person is working on, so they won't even see that something is broken. And the symptoms will be nonsensical, like a hanging progress indicator or something.

2024-08-22T17:04:57.720149Z

I think what you are getting at is you want some way to have it where you do not have to care at all if an event handler does something sync/immediately vs later when you are in some other fn/handler and calling out sequences of dispatches. But you will still have dependencies - so they have to be dealt with. So your handlers will just have to be checking and waiting for something one way or another. I’ve done this before with things like “busy loops” in a handler even. Where a handler checks if some app-db state is “loaded yet”, if it is “loading”, it just dispatches itself later after some time has passed. This sort of decoupled the need for one handler to notify another handler directly - more of an “bus” sort of decoupling. This just was clunky too and async-flow made it clearer and more explicitly managed.

emccue 2024-08-22T17:05:06.027869Z

ugh

emccue 2024-08-22T17:05:10.428899Z

please stop saying its name

emccue 2024-08-22T17:05:48.889839Z

it made stuff so bad and I want you to understand exactly how

2024-08-22T17:06:08.703319Z

I’m just really lost in what is being proposed. I do appreciate the discussion though.

2024-08-22T17:06:53.729859Z

We use :async-flow/notify intentionally to make it clear how signaling works at a broader level than perhaps individual events. It’s quite useful to not tie it to actions directly. We may not want to do an action if it has already happened, but we can just send the notify as an indicator that nothing needs to be done.

p-himik 2024-08-22T17:07:17.706659Z

> assume dispatches have their effects fully done in that “cycle”. Alas, it's not something you can assume with re-frame's events. But it is something you can assume with core.async or JS promises. There's a very explicit "end" to every go block, to every function producing a channel, to every promise.

emccue 2024-08-22T17:07:40.736309Z

I can jump straight to where we ended on this but i have a feeling you'll blow it off if i don't go through the full problems list

2024-08-22T17:07:42.252039Z

If you start cherry picking an event that has dependencies of a flow out of the flow - perhaps you could get trouble. I just don’t see how that is a practical scenario where you are using event handlers in random places without understanding its dependencies.

emccue 2024-08-22T17:09:59.204549Z

okay

emccue 2024-08-22T17:10:01.566979Z

anyways

2024-08-22T17:10:52.478679Z

I’m not blowing things off, I’m just not seeing the “light” basically. I do think there are alternative ways to model async flow dependencies than using event dispatches. I think async-flow chose that route and it hasn’t came up as a problem in quite a few real world cases for us. We haven’t seen it ever get carried away and unmanageable either. I think conventions can help perhaps problems like people trying to use handlers with dependencies without wiring up their dependencies first. I guess it’d be interesting if instead there was some concept of more explicit handler deps declared on a handler itself in some way that would be a framework to trigger loading what it may need if those things haven’t been loaded. This is an interesting takeaway to me. something like [:x :depends-on :y] where if :x is triggered directly at some point, there is some intelligence that knows to ensure :y has been done first. - Maybe more of a “flow” that instead goes the opposite direction and I have not thought at all of how that could be represented impl wise.

emccue 2024-08-22T17:11:15.356619Z

So as we've established, long chains of dispatches can have an adverse effect on the refactorability of code and, in the codebase I was working in, every http request meant like ~3 dispatches

emccue 2024-08-22T17:11:32.294169Z

Now lets introduce some more aspects

emccue 2024-08-22T17:13:05.899369Z

1. Every page of the app wanted different data 2. Each bit of data had its own route. In general they didn't want to make "route for page a" and instead made routes per "thing"

emccue 2024-08-22T17:13:15.895379Z

you can see this in the async flow I shared above

emccue 2024-08-22T17:13:19.376529Z

{:when       :seen-all-of?
                        :events     [(fn [& args]
                                       (and (apply (supported-for-user-type-matcher have-creator) args)
                                            (apply (routing/legacy-advertiser-route-changed-matcher collab-routes) args)))]
                        :dispatch-n [[::collabs/fetch-collaborations]
                                     [::collabs/fetch-collaborating-advertising-links]
                                     [::collabs/fetch-collab-participants]
                                     [::advertiser/fetch-team-creators]
                                     [::collabs/fetch-terms]
                                     [::collabs/completed-fetching-collabs]]}

emccue 2024-08-22T17:13:42.662949Z

when we hit a page that is a "legacy-advertiser-route", dispatch these 6 events

emccue 2024-08-22T17:14:33.955689Z

each of those 6 events would make an http request, set a loading flag in the db, and dispatch maybe more events

emccue 2024-08-22T17:15:18.385349Z

So obvious question - how do you know that the whole process was done? We have 6 or more independent things loading

emccue 2024-08-22T17:16:34.918909Z

basically every view ended up with

(when (and @(rf/subscribe [::stuff])
           @(rf/subscribe [::other-stuff])
           ...)
  [:p "hello"])

emccue 2024-08-22T17:16:46.973059Z

"if the data is non-nil, we got it. good to go"

emccue 2024-08-22T17:18:08.397199Z

problems this created • Those requests can fail and then the page just loads forever in the best case, white screens in the worst case • Sometimes you would check that one request of the N required for a page was done and assume that all of them were done. In dev you have no issues then boom, random crash in prod

emccue 2024-08-22T17:18:16.505099Z

then also

2024-08-22T17:18:24.696959Z

> Have you tried e.g. this? https://lucywang000.github.io/clj-statecharts/docs/integration/re-frame/ @p-himik read this. Surface level seems nearly the exact same as how we think of async-flows using notify events for the “state names”. My guess as to why this is preferred is: 1) statecharts is a broader and popular way of organizing this sort of thing. 2) I’m going to guess it doesn’t manage the actual actions and transitions as re-frame dispatches? I don’t see anything saying that it does or doesn’t though.

emccue 2024-08-22T17:18:59.838769Z

Because the logic for "boot up a page" was in async-flow it would only run once. Once. Even if you changed pages and came back we wouldn't refetch any data by default

2024-08-22T17:19:31.680519Z

We’d just have all of those dispatches dispatch a notify event of what they were done - then a new async-flow step that looks for all success - for “loaded case” or any failure for “something failed” case and we’d organize a failure response if needed etc.

emccue 2024-08-22T17:19:53.122099Z

there were a few places where someone found how to re-run the event cascade again, but it was always a mess and a terrible substitute for normal control flow

emccue 2024-08-22T17:20:19.286879Z

and really doesn't work for "initialize page state"

2024-08-22T17:20:35.915819Z

Ok, at this point I’ll say I can agree that a bunch of “nested dispatches” can have pitfalls and confusion as far as understanding when things happen etc. I’d like to see alternatives.

emccue 2024-08-22T17:20:36.416179Z

which is, coincidentally, the only thing that it seemed appropriate for at first

emccue 2024-08-22T17:21:06.760609Z

well, and this isn't perfect either, the alternative we came up with was :fx

2024-08-22T17:21:38.270029Z

You can just have a handler that deals with reload/refresh and uses another async flow or modifies the existing structure. I don’t see the big problem there. I still agree that understanding a bunch of event dispatches on the queue can eventually become difficult to conceptualize correctly.

emccue 2024-08-22T17:21:58.325179Z

its not highlighted too well in the documentation (or at least it wasn't), but you can have a :fx key in the return value of a reg-event-fx

emccue 2024-08-22T17:22:14.897679Z

this directly triggers side-effects without going through the event queue

2024-08-22T17:22:29.052859Z

A more recent re-frame addition. We now (basically) only use :db and :fx for our top-level effect map keys.

2024-08-22T17:22:50.084999Z

If your side effect is an async HTTP fetch though - how does that work out?

emccue 2024-08-22T17:23:23.450359Z

so instead of some events doing

{:db ...
 :dispatch ...}
and some just doing the db
{:db ...}
and some having some weird other evil thing
{:db ...
 :async-flow ...}
Every event returns just :db and :fx

emccue 2024-08-22T17:23:36.964749Z

banning :dispatch from being in :fx

2024-08-22T17:24:22.967729Z

It’s an interesting rule, but I feel like conceptually you still end up with and “async flow” just not done via dispatches - just something else instead. Async callbacks or something?

emccue 2024-08-22T17:24:49.097619Z

for http requests we did something like this

emccue 2024-08-22T17:24:54.329619Z

(defevent get-line-items:success
  [response]
  :handler
  (fn [{:keys [db]}]
    (let [line-items response]
      {:db (-> db
               (assoc-in db:quickbooks-line-items line-items)
               (assoc-in db:quickbooks-line-items-loading false))})))

(defevent get-line-items:failure
  [_response]
  :handler
  (fn [{:keys [db]}]
    {:db (assoc-in db db:quickbooks-line-items-loading false)
     :fx (micro-toast-fx/open-danger-toast {:message "Could not get QuickBooks line items."})}))

(defn get-line-items-request
  [{:keys [generic-team-id on-success on-failure]}]
  {:method     :get
   :uri        (str "/api/generic-team/" generic-team-id "/quickbooks/line-items")
   :on-success on-success
   :on-failure on-failure})

(defevent get-line-items
  []
  :handler
  (fn [{:keys [db]}]
    (let [generic-team-id (-> db :team :generic-team-id)
          qb-integration  (boolean (get-in db db:quickbooks-integration))]
      {:db db
       :fx (when qb-integration
             (http-fx/transit-request
               (get-line-items-request {:on-success      get-line-items:success
                                        :on-failure      get-line-items:failure
                                        :generic-team-id generic-team-id})))})))

emccue 2024-08-22T17:25:09.899789Z

ignore defevent for now, pretend it said reg-event-fx

emccue 2024-08-22T17:25:40.816329Z

We would give functions to call with the result of a request that return an event vector to dispatch

emccue 2024-08-22T17:26:25.082289Z

the goal was to make every re-frame event real

2024-08-22T17:26:41.642199Z

This forms a coupling where you have to explicitly chain things right?

emccue 2024-08-22T17:26:48.100449Z

meaning if an event happened it was because of some outside force. An http request finished, a user clicked a button, etc

2024-08-22T17:26:56.344399Z

get-line-items:success may actually need to now trigger more events that need to know when it is done.

emccue 2024-08-22T17:27:09.859859Z

not because of a synthetic code structure choice

emccue 2024-08-22T17:27:21.624029Z

> get-line-items:success may actually need to now trigger more events that need to know when it is done. No it does not

emccue 2024-08-22T17:27:37.998849Z

for the same reason that splitting (inc (* n 3)) is unneccesary

2024-08-22T17:27:50.377049Z

How do you represent having an handler that is going to rely on the on-success of get-line-items:success having already been done?

emccue 2024-08-22T17:27:59.220429Z

thats not a real event

emccue 2024-08-22T17:28:38.184529Z

you would roll up all the state changes and side effects you want to trigger into get-line-items:success. If you need to, breaking them into helper functions. Because :db and :fx are composable

2024-08-22T17:29:20.539799Z

yeah, we had this type of things before though and felt it still got confusing and hard to scale.

emccue 2024-08-22T17:29:25.085709Z

we would do this for our route changed handler

emccue 2024-08-22T17:29:28.298249Z

(rf/reg-event-fx
  ::route-changed
  (fn [{:keys [db]} event]
    (let [change-route-key (fn [{:keys [db]} [_ next-route]]
                             (let [current-route (route db)]
                               {:db (cond-> db
                                      current-route (update-in [route-state-key :previous-routes]
                                                               (fn [history]
                                                                 (cons current-route history)))
                                      :always       (assoc-in [route-state-key :current-route] next-route))}))
          composed-handler (fx/compose-effect-fns #(change-route-key {:db %} event)
                                                  #(route-change-handler {:db %} event))]
      (composed-handler db))))

2024-08-22T17:29:32.962099Z

We’d then have perhaps 4 layers of things that need to happen in sequence, but have to wait on each other

2024-08-22T17:30:06.075369Z

So each of the 4 had to be explicitly linked together. Step 1 had to know about Step 2. Step 2 had to know about Step 3. Step 3 had to know about Step 4. You can do this without dispatches or with it, same overall issue though.

emccue 2024-08-22T17:30:13.183199Z

where every time the route changed we would do some common db changes and side effects but then also include a custom function for that route

2024-08-22T17:30:51.296439Z

Yeah, I think we have some similar code to this in a few places.

emccue 2024-08-22T17:31:28.277049Z

oh almost forgot how "helper events" made 10x useless

2024-08-22T17:31:29.330349Z

We compose fx, we add fx as args to the initial dispatch to know what to “do next” etc. I think this is what I was getting at with a “continuation” sort of feel.

2024-08-22T17:32:04.231369Z

with or without async-flow, I never had really high value in 10x event logging. Could be just naive there. I mostly just like it for the app-db view.

emccue 2024-08-22T17:33:22.587359Z

the idea was • If a re-frame event happens, it should be in response to something in "the real world" happening, otherwise its horrible to reason about • If we use async-flow for page state, we have problems managing both "replaying" those events when someone gets back on the page and predicting the results of those events. So just have each page maintain its own state with normal events + a route changed handler to set up a valid "zero" state besides nil

2024-08-22T17:33:48.454999Z

It seems to me though that the real crux of the problem here is that async-flow uses events to coordinate the flow. You can instead just chain :fx order in a particular way to capture something a bit similar. You can have “waiting effects” in a sense with that sort of “continuation passing style”. This may be better than using events for coordination because perhaps the re-frame event queue is just too opaque for good reasoning.

emccue 2024-08-22T17:36:06.812259Z

seperately, defevent was nice too

emccue 2024-08-22T17:36:32.799329Z

when you dispatch a reframe event you need to give the right args in the vector which you have little/no ide support for

👍 1
emccue 2024-08-22T17:36:40.888819Z

and events can't really be documented in the same way

2024-08-22T17:36:44.150379Z

It’s been interesting conversation for me. Oddly enough, we really have no issues using async-flow as I already said and it just was an experiment we used early on a while back and everyone was happy with the simplification of things. I’m quite curious to see how https://lucywang000.github.io/clj-statecharts/docs/integration/re-frame/ is implemented if it isn’t through a bunch of dispatches and I’d like to see how it works in complex flows.

2024-08-22T17:37:02.592959Z

Yeah, I like the idea of the defevent. I’ve heard ideas like this thrown around. I think nothing is open sourced? We have a few internal wrapper helpers, but not some that I’d like to see.

emccue 2024-08-22T17:37:58.195249Z

let me check exactly how defevent worked

2024-08-22T17:38:01.990489Z

I mentioned in some other thread here a bit back that we have done quite a bit over “subscriptions” that has helped us simplify a lot of flows and also took performance into consideration. Was here https://clojurians.slack.com/archives/C073DKH9P/p1723137501762649?thread_ts=1722864507.061879&cid=C073DKH9P

2024-08-22T17:38:46.660469Z

I know we’ve blown this thread way out of proportion. Can discuss these topics in a separate one if that helps. (like defevent)

emccue 2024-08-22T17:39:22.215489Z

here was the docstring

emccue 2024-08-22T17:39:23.950709Z

"Defines an event. Expects input in one of these forms

  (defevent event-name
    [arg-1 arg-2]
    :handler
    (fn [cofx]
      {:db ... :fx ...}))

  (defevent event-name
    \"docstring\"
    [arg-1 arg-2]
    :handler
    (fn [cofx]
      {:db ... :fx ...}))

  (defevent event-name
    [arg-1 arg-2]
    :interceptors
    [interceptor-1 ...]
    :handler
    (fn [cofx]
      {:db ... :fx ...}))

  (defevent event-name
    \"docstring\"
    [arg-1 arg-2]
    :interceptors
    [interceptor-1 ...]
    :handler
    (fn [cofx]
      {:db ... :fx ...}))

  And generates the following
    - a constant named event:event-name which is a namespaced keyword
    - a function named event-name which returns an event vector which can be dispatched with re-frame
    - a function named handler:event-name which takes the coeffects and an event vector and returns :db, :fx
    - the call to register the handler:event-name function to handle events.
      - by default this registration is done globally inside of re-frame
      - if you use the register-with flag, it will instead assoc itself into the given atom
  "
  [& args]

👍 1
emccue 2024-08-22T17:40:14.437329Z

having handler:event-name helped if we needed to compose two events without introducing a dispatch

👍 1
2024-08-22T17:40:59.410899Z

Nice. I want to think about this one some more. Compelling.

2024-08-22T17:41:32.581459Z

I think we often do that sort of thing by just manually removing the implementation into a common function to use. But I could see it being nicer this way.

emccue 2024-08-22T17:42:40.293669Z

its one of the few macros that i've written that actually ended up being a net positive

👍 1
emccue 2024-08-22T17:42:53.597339Z

not net positive enough to save the code base, but 🤷

☠️ 1
emccue 2024-08-22T17:43:48.429099Z

anyways @james.luke.johnson We had this helper for initial loading states

emccue 2024-08-22T17:44:06.736639Z

(defn create
  "Creates an object that can be used to track the status of multiple requests on a page.

  :resources
  a collection of the names of resources that need to load."
  [{:keys [resources]}]
  {:things-still-loading  (set resources)
   :things-that-succeeded #{}
   :things-that-failed    #{}})

(def status:failed ::failed)
(def status:success ::success)
(def status:loading ::loading)

(defn overall-status
  "Returns the overall status of the process.
  Assumes that any failure means the whole process was a failure"
  [initial-loading-process]
  (cond
    (seq (:things-that-failed initial-loading-process))
    status:failed

    (seq (:things-still-loading initial-loading-process))
    status:loading

    :else
    status:success))

(defn status
  "Returns the status of an individual resource."
  [initial-loading-process resource]
  (cond
    (contains? (:things-that-failed initial-loading-process) resource)
    status:failed

    (contains? (:things-still-loading initial-loading-process) resource)
    status:loading

    :else
    status:success))

p-himik 2024-08-22T17:44:16.382599Z

@mikerod > 1) statecharts is a broader and popular way of organizing this sort of thing. And it's also a proper state machine, without any need for its users to come up with a bespoke convention. > 2) I’m going to guess it doesn’t manage the actual actions and transitions as re-frame dispatches? I don’t see anything saying that it does or doesn’t though. Not sure how to interpret the words "manage the actions and transitions". It does dispatch events. But it doesn't monitor events to see what needs to be done. You specify states and transitions not via "if this event is fired, do this" but via an explicit state description. Dispatched events then become not a management mechanism but a side-effect.

emccue 2024-08-22T17:45:11.298879Z

and we would slap it on page state

emccue 2024-08-22T17:45:22.975939Z

(defn initial-state
  [{:keys [user-kind collaboration-uuid]}]
  {:initial-loading-process         (initial-loading-process/create
                                      {:resources (cond-> #{thing-to-load:public-info}
                                                    (= user-kind user-kind:creator) (-> (conj thing-to-load:private-note)
                                                                                        (conj thing-to-load:advertising-links)
                                                                                        (conj thing-to-load:collab-info)))})
   :file-upload-status              {}
   ...})

emccue 2024-08-22T17:46:34.267939Z

then when requests finish their success/failure events would update their state in the bucket. Once everything succeeded you could move forward with rendering the page.

emccue 2024-08-22T17:48:42.289459Z

the way i think about this stuff will make a lot more sense if you try writing a program in Elm

emccue 2024-08-22T17:49:10.250599Z

i don't know the directionality of influence, but in elm you are forced to do things more or less the way we ended up doing them near the end

2024-08-22T17:49:47.380219Z

I’ve read a lot on elm but never done anything big.

2024-08-22T17:57:48.510009Z

> Not sure how to interpret the words “manage the actions and transitions”. > It does dispatch events. But it doesn’t monitor events to see what needs to be done. You specify states and transitions not via “if this event is fired, do this” but via an explicit state description. > Dispatched events then become not a management mechanism but a side-effect. If you use notify events it does decouple the “action” from the “transition” - so I think it is somewhat accomplished by async-flow with that pattern. It is not a proper state machine in a sense - it’d be weird to write a flow where a state could transition “backwards to a previous state” for example. I have to think if that really helps anything though. I think with the state machines approach there is an argument for your original point that you don’t want event handlers that may accidentally be called standalone when they were only meant to be part of a flow with dependencies. These state machine actions aren’t explicit handlers so you can’t mistakenly try to use them “standalone”. I think this is what I meant by conventions can accomplish the same thing, but you don’t like that and think it’s too brittle. However, you may actually want to reuse actions where the states/transitions vary, so you’d just have to use different state graphs for different initializations of that I guess.

2024-08-22T17:58:11.573699Z

I’m fine to leave it at this for now though. This is a lot of typing and responding and I appreciate the feedback and takeaways to consider.

DrLjótsson 2024-08-22T18:10:18.026309Z

@james.luke.johnson, I would suggest that you use a counter instead of a boolean :loading? key. When an http request starts, you inc the counter, and when it ends, you dec it. Whenever it’s pos?, something is loading.

emccue 2024-08-22T18:12:15.572669Z

set of things loading, set that finished ok, set that failed

👍 2
emccue 2024-08-22T18:12:34.950559Z

counters are begging for shenanigans

Luke Johnson 2024-08-22T18:19:55.705409Z

Thanks for a great idea! That seems much better than a boolean. TL;DR if you don’t feel like reading the middle 205 comments in the thread 😅 The answer to my initial question is: Best practice is to use a helper function to operate directly on the db instead of dispatching an event to do the db update. Specifically for keeping track of loading, consider using a set of elements loading instead of a boolean.

👍 1
Kimo 2024-08-25T15:23:56.513949Z

Any opinions on this pr? https://github.com/day8/re-frame-async-flow-fx/pull/32 And generally, @emccue, do you think any of these issues could be solved through incremental changes to re-frame-async-flow-fx?

👀 1
emccue 2024-08-25T15:26:33.014469Z

Well aside from the "only run once" stuff which that PR would address: the issues around "unpredictability of synthetic event chains" remain

M 2024-09-15T21:20:34.754819Z

Long thread which I didn't want to read. My vote is that you would set the state on the db directly when the loading starts, but dispatch when the loading ends. This would be to set the widest range that the UI wouldn't have the right info to show.