Fork me on GitHub
#re-frame
<
2021-09-07
>
pez08:09:46

If I have a Layer-3 subscription and want to cause a side effect (post towards a remote API in this case) when the subscription changes, is that possible? Right now I trigger this effect at each signal subscription, and it is a bit inconvenient and easy to forget. I hope the code snippet manages to explain what I fail to relay using prose. 😃

pez08:09:29

If someone sees some anti-pattern or such here, please let me know. What’s maybe not obvious here is that I subscribe to :context in remote-service/update-context!, but that should be fine, right? (At least it works, haha.)

p-himik08:09:40

The easy way would be to use reg-sub-raw and dispatch from it. But re-frame docs explicitly recommend against that approach. The right way would be to create a global interceptor and there monitor if the data used by the :context sub has changed, and if so, to update the remote service. Alternatively, extract the :context handler function to a separate function, call it in some global interceptor on any data change, store its result in app-db, and call :update-remote-service whenever that cached data has changed. Given that you subscribe to :context in the effect handler, definitely go with the latter approach. Do not use subscribe anywhere but in the view render functions. You should not use it even in the JS event handler functions - regardless of whether they're defined within views or not.

pez08:09:24

Hmmm, I also subscribe to a thing in the .on of an event emitter I use… I know I have only one and that it will never be recreated. Is it still no-no?

p-himik08:09:50

It creates a memory leak. In practice, that might not be important at all, if the subscription is always in use and always with the same argument vector.

p-himik08:09:32

Ah, and apart from the mere memory leak, I think such subscriptions will be re-evaluated even if they're not in use.

p-himik08:09:22

If you don't use that sub anywhere but in that .on, it might be better to move it to some event handler. But can't really tell without seeing the actual code.

lsenjov09:09:38

Subs which aren’t used in active rendered components aren’t kept reactive, so won’t be tracked. If you really want to go down that route, there is a track! function for subs that keep that sub reactive.

👍 4
pez10:09:45

Thanks. Sounds like I need to re-consider quite a few things. Regarding my sub in the .on handler, it’s setup like so:

(defonce my-emitter
  (new EmittingThingy (clj->js {...})))

(defn- re-configure [x]
  (let [status (.getStatus my-emitter x)]
    (rf/dispatch [:update-config x status])
    status))

(defn start! []
  (.start my-emitter)
  (.on my-emitter "update"
       (fn on-emitter-update []
         (doseq [x (-> @(rf/subscribe [:config]) (keys))]
           (re-configure x)))))
I call start! as part of the application start sequence. Basically it is that I keep track of this config and when my-emitter emits update I need to query it for the things I am interested in. (All this is code I have just written and nothing is in production, so I am free to change anything.)

p-himik10:09:09

So you have my-emitter and re-frame combined at the level of your main application code. I would move it all to a separate namespace with a few effects and events that would deal with re-configuration and initialization of the emitter. If the config is something that only the emitter needs and you wouldn't consider it a part of the application's state (something you might want to log, instrument, undo/redo, etc), I would also move it from app-db into its own atom.

pez11:09:14

It’s very much part of the application state. It’s feature-flags infrastructure. Not sure I follow about that my-emitter and re-frame are combined? I mean, obviously I use both, but I see it as I am using the emitter to inform my application state. re-frame is just my vehicle. I am probably miss-understanding something critical here. Anyway, I don’t know how to tease it apart. I’ll ponder it some. This is just a prototype for designing the interface for the developers using the system, but I will later move this to the real application and then it gets crucial that I get it simple and robust.

pez11:09:26

I guess I could put it like this: I do see that things are complected, but it’s unclear how and what to do to uncomplect.

p-himik11:09:16

By "combined" I meant that you glue them together as if that gluing itself is part of your business logic. Hence, I'd move it to some fx.cljs and make the rest of the application talk to it only via events that return relevant effects.

p-himik11:09:40

(defonce my-emitter ...)
(defonce on-emitter-update (atom nil))

(rf/reg-fx :reconfigure-emitter
  (fn [config]
    (reset! on-emitter-update
      (fn []
        (run! re-configure (keys config))))))

(rf/reg-fx :start-emitter
  (fn [_]
    (doto my-emitter
      .start
      (.on "update" (fn []
                      (when-some [f @on-emitter-update]
                        (f)))))))

(rf/reg-global-interceptor
  ... an interceptor that watches the emitter's config in app-db and dispatches an event that returns the :reconfigure-emitter` effect with the right argument ...)
Something like that.

Fahd El Mazouni08:09:03

Hey there peeps ! could anyone recommend a clean zprint conf for reframe projects ?

Simon11:09:46

dispatch-n doesn't complete execution of first event before the next, how do i enforce this? How do i make sure that this dispatches in this order:

[:initialize-db] 
[:re-calculate]  ; depends on [:initialize-db]
[:update-cashflows] ; depends on  [:re-calculate]
[:update-current-city-fire-number] ; depends on [:initialize-db]
[:update-current-city-time-to-retirement] ; depends on [:update-current-city-fire-number]
[:initialize-cities] ; depends on [:update-cashflows]
Right now it executes in this order: Current order:
[:initialize-db]
[:re-calculate]
[:initialize-cities] ;; Error: Should be executed after [:update-cashflows]
[:update-cashflows]
[:update-current-city-fire-number]
[:update-current-city-time-to-retirement]
[:set-current-location "Copenhagen"]
it is dispatched here:
(rf/reg-event-fx
 :initialize-db
 (fn [_ _]
   {:db initial-app-db
    :dispatch-n (list [:re-calculate]
                      [:initialize-cities])}))

(reg-event-fx
 :re-calculate
 (fn [_ _]
   {:dispatch-n (list [:update-cashflows]
                      [:update-current-city-fire-number]
                      [:update-current-city-time-to-retirement])}))
So the problem is that dispatch-n doesn't complete execution of the first event (`[:re-calculate]`) and its subevents before it continues to the next (`[:initialize-cities]`)

p-himik11:09:58

All the events are completed in order. But I'm gonna guess that your first event returns some :dispatch as well - and that event will not get executed, it will be queued up. It has been suggested before that you should not do this in the first place. Instead, extract the event handlers' code and compose them as regular functions. A small thing - you don't have to use (list ...), you can just wrap the vectors in another vector.

Simon12:09:29

Thank you @U2FRKM4TW i see that the code from your gist could be a viable solution:

(rf/reg-event-fx
  ::something
  (fn [{:keys [db]} [_ foo bar]]
    (-> (do-thing {} foo)
        (do-another-thing bar)
        (do-one-last-thing))))
(edited)

ribelo12:09:22

@U2FRKM4TW This is the umpteenth time I've seen advice not to use events as a function, but why? After all, :fx is perfect for this in theory.

Simon12:09:47

Something that might be relevant

ribelo12:09:27

@U024A5W9WBG but there is nothing here to compose functions, but to combine small events as if they were functions

Simon12:09:46

so i have to use this method?

(defn re-calculate [db]
  (-> (update-cashflows db)
      (update-current-city-fire-number)
      (update-current-city-time-to-retirement)))

Simon12:09:01

(defn initialize-db [db]
  (-> initial-app-db
      (re-calculate)
      (initialize-cities)
      (calculate-top-city)))

ribelo12:09:38

I don't know what you have there exactly, but instead of :dispatch-n just use :fx

ribelo12:09:23

just remember that if an event calls another event, it lands at the end of the queue, and is not called sequentially like normal functions

Simon12:09:08

and :fx will be able to take a vector of events?

Simon12:09:18

Since i have to execute it in the order mentioned above i think i have to define a new function that follows this order instead of dispatching multiple events.

Simon12:09:08

I have it working now using that method.

Simon12:09:46

The only drawbacks being that you can't inspect them as individual events in e.g. re-frame-10x

ribelo12:09:16

I didn't paste everything into pastebin earlier

ribelo12:09:45

yes, you will be able to track all events in re-frame-10x

ribelo12:09:39

re-frame-10x tracks each event trigger

Simon12:09:16

i was refering to my method of chaining together the functions instead of the events.

ribelo12:09:18

recommend you take a look @U024A5W9WBG

ribelo12:09:55

This is probably the largest project available that uses a re-frame

Simon12:09:59

Code from @U0BBFDED7 pastebin link:

(defn fn-aa []
  (println :aa))

(defn fn-a []
  (println :a)
  (fn-aa))

(defn fn-bb []
  (println :bb))

(defn fn-b []
  (println :b)
  (fn-bb))

(do
  (fn-a)
  (fn-b))
;; prints
;; :a
;; :aa
;; :b
;; :bb

;; --------------

(rf/reg-event-fx
 ::a
 (println :a)
 {:fx [[::aa]]})

(rf/reg-event-fx
 ::aa
 (println :a))

(rf/reg-event-fx
 ::b
 (println :b)
 {:fx [[::bb]]})

(rf/reg-event-fx
 ::bb
 (println :bb))

:fx [[::a]
     [::b]]
;; prints
;; :a
;; :b
;; :aa
;; :bb
Did you mean to call ::aa both in :fx and inside the (defn a ... ?

ribelo12:09:52

no, this just shows the difference in call order between functions and events

Simon12:09:46

ah sorry and yes now i realize. But in my case i have to get the execution order in the top example. So i guess i have to use normal chaining of functions

Simon12:09:44

As I've already done and it works fine. But again has the drawback that there are no events in re-frame-10x

ribelo12:09:01

:fx is sequential and preserves order

ribelo12:09:20

I can't tell you more without the code

ribelo12:09:15

create gist or pastebin and maybe I'll tell you more about why the order is not correct

🙏 2
Simon12:09:24

My code is in the top of this thread. What are you missing?

Simon12:09:24

So given your example from pastebin above, how do i enforce the order

:a
:aa
:b
:bb
by using events? My current solution achieves this using functions, but again the drawback is this might make debugging harder in the future, since it will only show one event in re-frame-10x

Simon13:09:53

Thanks for all the help so far to both of you. This question is mostly out of interest, but not super urgent.

ribelo13:09:55

I see no reason why it would be called in a different order than specified

[:initialize-db] 
[:re-calculate]  ; depends on [:initialize-db]
[:update-cashflows] ; depends on  [:re-calculate]
[:update-current-city-fire-number] ; depends on [:initialize-db]
[:update-current-city-time-to-retirement] ; depends on [:update-current-city-fire-number]
[:initialize-cities] ; depends on [:update-cashflows]

ribelo13:09:18

problem lies elsewhere

Simon13:09:39

:re-calculate corresponds to :a :update-cashflows , :update-current-city-fire-number and :update-current-city-time-to-retirement corresponds to :aa :initialize-cities corresponds to :b when I use events the order will be :a :b :aa as you also show in your example on pastebin. I want the order :a :aa :b

p-himik13:09:10

@U0BBFDED7 > This is the umpteenth time I've seen advice not to use events as a function, but why? After all, `:fx` is perfect for this in theory. Specifically because of the OP's question. It's much harder to reason about the order of things when you have a queue of events vs. when you have a data that's changed right there. Using :dispatch-n or :fx with multiple :dispatch effects is fine when those events don't depend on each other. But when they do, it's very easy to shoot yourself in the foot, and it's very hard to figure out what's wrong. > This is e.g. a bit contradictory to what can be found here Not contradictory at all because that page deals with a very specific problem. Apart from that, just using :fx or :dispatch-n won't solve a thing there for reasons described under the "So close. But it still won’t work. There's a little wrinkle." line.

🙌 2
p-himik13:09:05

When event :b depends on the result of event :a, the solution that's the easiest to reason about is to compose the functions that do the actual work. Not something built on top of such functions that also changes the model of how things are computed.

🙌 2
ribelo13:09:17

I just caught what the op meant.

ribelo13:09:56

Sorry @U024A5W9WBG for misleading you

Simon13:09:02

Thank you for both your patience! Finally we understand each other 😄

ribelo13:09:46

Anyway, I see it's all about the app launch

emccue18:09:49

@U0BBFDED7 please don't use or suggest async flow

emccue18:09:34

i beg of you - the absolute worst code in our codebase lives there and we are just finally able to start to get rid of it

emccue18:09:26

it is hypothetically fine - the library does exactly what it says it does - but wanting what it does is a trap

ribelo18:09:39

I see that this is maintained, and the latest version is really fresh by clojure standards

emccue20:09:24

yeah - its nothing wrong with the code or the maintenance thereof

emccue20:09:21

its just that while it technically works for bootup tasks it encourages some really loose modeling of dependencies and really falls apart if you try to use it to "boot up" a page

Oliver George22:09:19

My thoughts on the subject of "events as utils" and an alternative (compose!): https://gist.github.com/olivergeorge/edc572eab653cc228ec5ecbbcc8271a2

Oliver George23:09:08

Tweaked that gist to include a spec and take out the "this is what emccue says" bit :-)

AJ Jaro01:09:06

async-flow does great for bootup, but it’s difficult to hold yourself back from wanting more. It’s very tempting to think that it can solve some other state management situations. In addition, it’s a very easy-to-use interface

Simon17:09:35

How do i force a re-render of mapbox-autocomplete-component every time the city-name updates. Currently it doesn't react (pun intended) to this:

(defn- on-select [option on-suggestion-select]
  (let [city (get-in (js->clj option :keywordize-keys true) [:city])
        coordinates (:center city)
        lon (coordinates 0)
        lat (coordinates 1)
        place-name (:place_name city)]
    (on-suggestion-select place-name lat lon)))

(defn- mapbox-autocomplete-component [{:keys [public-key city-name on-suggestion-select types]}]
  (let [value (r/atom city-name)
        options (r/atom [])]
    (fn []
      [:> AutoComplete {:style {:width "100%"}
                        :on-change #(reset! value %)
                        :value (or @value "Loading...")
                        :options @options
                        :on-search #(on-search % options public-key types)
                        :on-select #(on-select %2 on-suggestion-select)}
       [:> Input {:placeholder "Current City" :allow-clear true :size "large"}]])))


;; main component

(defn current-city-form-field []
  (let [city-name @(rf/subscribe [:geolocation :city_name])]
    [:> (.-Item Form) {:label "Current City" :name "current-city"}
     [:div
      [mapbox-autocomplete-component
       {:public-key MAPBOX-PUBLIC-TOKEN
        :types ["country", "place"]
        :city-name city-name
        :on-suggestion-select #(rf/dispatch [:set-current-location %1 %2 %3])}]]]))

p-himik17:09:24

One way, which is a tad of a hack, is to wrap it inside another component and add ^{:key city-name} in front of [mapbox-autocomplete-component ...]. The key metatada will become React key and will force React to re-mount it when city-name changes. Another, more proper, way is to create a form-3 component and do the right thing in :component-did-update. Yet another thing, which is somewhere in between, is to add an additional ratom to track the changes of the external model. Just like this block does here: https://github.com/day8/re-com/blob/07451b1d19c59eb185548efe93e2d00b5d3eab89/src/re_com/input_text.cljs#L97-L99

🙌 2
👏 2
ribelo17:09:17

@U024A5W9WBG in general, many things make no sense in what you wrote

ribelo17:09:33

mapbox-autocomplete-component takes a city-name, then wraps it in an atom, then does a deref on it

p-himik17:09:04

That's because it also resets that ratom - it's an internal model specifically for the auto-complete component. Not that uncommon, but an incomplete implementation.

p-himik17:09:27

Re-com, that I linked above, also has its own auto-complete component - might be useful to take a look at.

ribelo17:09:14

I see, but it would probably be more appropriate to keep it in the db

p-himik17:09:26

BTW the :key solution/hack probably won't work here because it will not just re-render the component, but re-mount it, thus losing all the internal state.

ribelo17:09:28

and on-change call the event

p-himik17:09:00

> it would probably be more appropriate to keep it in the `db` Depends on whether someone considers the temporary input to be a part of the app's state. Seems like mapbox-autocomplete-component doesn't know and doesn't care about re-frame. It's a component that can be reused in any Reagent app. If that works and is desirable, it's perfectly fine to not use app-db for that.

Simon17:09:24

Yes that is what i thought. I want to make all components as re-useable as possible. Thank for the brilliant hack using the ^{:key city-name}!

ribelo17:09:59

@U2FRKM4TW You are 100% right, but still, keeping everything in db solves most of the problems, ...and creating many others : )

p-himik17:09:05

Exactly, still a lot of trade-offs have to be figured out. Alas, that's the case for the whole UI business right now.

Simon17:09:35

Well said

Simon17:09:12

When you mentioned :component-did-update did you refer to something like this: https://purelyfunctional.tv/guide/re-frame-lifecycle/

p-himik17:09:01

I referred to React's lifecycle methods, yes. Re-frame is built on top of Reagent. So to use re-frame effectively, you gotta know Reagent somewhat well. Not its insides and outs, but just how to use it. Reagent, in turn, is built on top of React - so same applies here. You gotta know React somewhat well. The most useful concepts are lifecycle methods, key, ref, the difference between elements, components, and instances. Unless I'm forgetting something, the rest is less important.

☝️ 2
Simon17:09:19

I am aware of the life-cycle methods, although they i haven't used them a lot since i focus on writing functional components instead of classes. AFAIK key is mostly for differentiating items in a list https://reactjs.org/docs/lists-and-keys.html. Although i'm not sure i understand why it is usable in this context. I would assume mostly to differentiate that the component did change and cause a re-render. Also what kind of ref are you referring to? A HTML node?

p-himik17:09:55

> i focus on writing functional components By default, Reagent creates class components out of all your functions. ;) Keys are for uniquely identifying elements within their parent. If a key is the same between two vDOMs, then it's the same element - no questions asked. If it's different - it's different, even if the Hiccup is the same. By refs I meant this: https://reactjs.org/docs/refs-and-the-dom.html It will be an HTML node only if the component on which you set :ref is itself rendered as an HTML node. But you can't rely on that in the general case.

Simon17:09:03

Thanks @U2FRKM4TW definitely worth a re-read.

emccue21:09:14

@U055DUUFS I'm not sold on always composing functions that do :db -> {:db, :fx} . Personally I think its usually better to write helpers for the db only and explicitly write out :fx. Then in the rare(er) cases where you want to compose two things that are :db -> {:db, :fx} just compose the functions, don't make the functions built around threading and updating a single map

emccue21:09:07

for us we have page containing all our view code, page.model containing the functions that work on the page-state and then page.events that has all our events and functions that need to dig deeper into the toplevel db

emccue21:09:52

and then occasionally page.partials.* for if we need to split up the html/css and are keeping the same model and events

emccue21:09:01

and the strategy for having truly reusable components is still evolving - but it is currently leaning toward having a view/model/event structure entirely separate from re-frame that can be embedded into the state of a page

Oliver George22:09:16

Sounds interesting. (Minor point: my actions are ({:db, :fx}, ...) -> {:db, :fx})

Oliver George23:09:13

Tweaked that gist to include a spec and take out the "this is what emccue says" bit 🙂