Fork me on GitHub
#integrant
<
2021-09-27
>
Dax Borges18:09:48

Hi I'm looking to use integrant on the frontend where I'm lazy loading in modules made up of components. The components need to be initialized in. a specific order. I'm trying to figure out how to use integrant for the following: I have three components comp-a,`comp-b`, and`comp-c` . Both comp-a and comp-b depend on comp-c and need it to be initialized be for they are. I have two modules, mod-a and mod-b which contain comp-a and comp-b, respectively. If I can't control the order of when the modules load is there a way to initialize comp-c in whichever module loads first and then use its reference in the other module (without initializing it a second time). For example, if mod-a loads first, comp-c and comp-a are initialized , then mod-b loads and only comp-b is initialized and the reference of comp-c from mod-a is used?

Kevin19:09:11

> For example, if `mod-a` loads first, `comp-c` and  `comp-a` are initialized , then `mod-b` loads and only `comp-b` is initialized and the reference of `comp-c` from `mod-a` is used? Yes, both comp-a and comp-b will use the same value returned by comp-c init-key. Meaning that comp-c only gets evaluated (initialized) once

Kevin19:09:04

It seems to me that the order is valid though. So I'm not sure what the problem in your scenario is

Kevin19:09:48

What are you using to render your components? (we use reagent with integrant at my work)

Dax Borges19:09:28

As I understand it we need to create two systems by calling ig/init in both mod-a and mod-b. If I don't control which system starts first how can share comp-c? Is there a way to merge systems?

Dax Borges19:09:03

We use re-frame with reagent, what I'm trying to do is to make sure events and subscriptions are loaded before they're used.

Kevin19:09:11

All right, if two components share 1 ref (in this case, comp-c) does comp-c need to emit an event twice?

Dax Borges19:09:23

no, I want to register the event handler once and in comp-c and then from comp-a and comp-b I want to be able to dispatch the event knowing that the handler exists

Kevin19:09:00

Writing up some pseudo code to see if I understand, one moment 😄

Dax Borges19:09:14

Appreciate it!

Dax Borges19:09:26

let me try to get some put together

Kevin19:09:20

To me it sounds likes you're doing something like this?:

(defmethod ig/init-key :comp/a [_ {:keys [comp-c]}]
  (fn []
    [:div "From A - Comp C: " comp-c]))

(defmethod ig/init-key :comp/b [_ {:keys [comp-c]}]
  (fn []
    [:div "From B - Comp C: " comp-c]))

(defmethod ig/init-key :comp/c [_ opts]
  (re-frame/dispatch [:initialize/comp-c opts])
  (let [comp-c (re-frame/subscribe [:db/comp-c]) ]
    (fn []
      [:div "Comp C: " @comp-c])))

Kevin19:09:22

So you have 3 (reagent) components. a and b reference component c. component C dispatches an event at initialization (once) and returns a reagent component (fn) with its own sibscriber

Kevin19:09:56

{:comp/a {:comp-c #ig/ref :comp/c}
 :comp/b {:comp-c #ig/ref :comp/c}
 :comp/c {}}

Kevin19:09:01

And this would be the config

Kevin19:09:32

(At my job we sometimes use ig/prep-key to dispatch events instead of ig/init-key )

Kevin19:09:23

But either should work. The events are only dispatched once and comp-c has access to the subscription within its fn context

Dax Borges19:09:12

more this

(defmethod ig/init-key :components/comp-a [_ {:keys [post-alert-event] :as opts}]
  (rf/dispatch [post-alert-event "comp-a alert"]))

(defmethod ig/init-key :components/comp-b [_ {:keys [post-alert-event] :as opts}]
  (rf/dispatch [post-alert-event "comp-b alert"]))

(defmethod ig/init-key :components/comp-c [_ _]
  (rf/reg-fx :post-alert (fn [_ [_ title]] ...trigger alert...)))

Dax Borges19:09:52

adding more to the example .. one min

Kevin19:09:15

Yeah I don't think this is reliable. re-frame/dispatch is not synchronous.

Kevin19:09:34

So the order of execution won't be set in stone by integrant (I think)

Dax Borges19:09:42

no it's the reg-fx that's synchronous

Dax Borges19:09:59

do I don't want dispatch to be called until reg-fx is

Kevin19:09:40

Right, but since dispatch is asynchronous I don't think it will wait

Kevin19:09:49

but I haven't tried this before

Kevin19:09:59

You've tested this and it doesn't work?

Dax Borges19:09:14

assuming comp-c init is called first that example will work fine, but I think we're getting to far into implementation details. Backing up the real issue I'm trying to figure out is how to control when comp-c is initalized if I want something like this

(ig/init {:comp/a {:comp-c #ig/ref :comp/c}
          :comp/c {}})

(ig/init {:comp/b {:comp-c #ig/ref :comp/c}
          :comp/c {}})

Dax Borges19:09:41

this part :comp/c {} is wrong

Kevin19:09:03

So you have multiple configurations?

Dax Borges19:09:39

yes, the real case is I have my app which calls ig/init and then the user navigates to some section the requires lazy loading a module , I then need to ig/init all components in that module but they often have deps that are already init originally by the app

Dax Borges19:09:05

does that make sense?

Dax Borges19:09:25

Really appreciate your help by the way! Trying to be concise and clear but also forming the picture in my head at the same time

Kevin19:09:25

I think what you're saying is that you want to initialize part of your config, and reuse the initialized parts later

Dax Borges19:09:50

yes, that's a better way of putting it

Kevin19:09:28

Yeah that's tough

Kevin19:09:50

You're going to have to write some wrapper code to make this work

Kevin19:09:18

I don't think integrant has any way of handling this situation. Integrant was created to build your entire system at initialization, not with lazyloading in mind

Kevin19:09:04

If you try to initialize a specific component, it will drop any other components that aren't referenced

(ig/init {:comp/a {:comp-c ( ig/ref :comp/c)}
          :comp/b {:comp-c ( ig/ref :comp/c)}
          :comp/c {}}
         [:comp/a])
;; => {:comp/c :c
;; =>  :comp/a [:a :c]}

Kevin19:09:15

But even if the keys (`comp/b` in this case) were still available, the next ig/init would fail because it would initialize :comp/c again instead of returning the value

Kevin19:09:20

A quick hack you could use is something like this...

Kevin19:09:37

Ok bear with me on this one 😅

Dax Borges19:09:53

hey, I appreciate any ideas you have!

Kevin19:09:04

(ns app
  (:require
   [integrant.core :as ig]
   [integrant-tools.core :as it]))

(set! *print-namespace-maps* false)

(defmethod ig/init-key :comp/a [_ opts]
  [:a (:comp-c opts)])

(defmethod ig/init-key :comp/b [_ opts]
  [:b (:comp-c opts)])

(defmethod ig/init-key :comp/c [_ opts]
  ;; This is only called once
  (println :initialize/comp-c)
  :c)

(defn call-subset [config ks]
  (let [config-subset (ig/init config ks)]
    (doseq [k (keys config-subset)] (remove-method ig/init-key k))
    (it/derive-unknown config-subset ig/init-key :it/const)
    (merge config config-subset)))


(let [config {:comp/a {:comp-c (ig/ref :comp/c)}
              :comp/b {:comp-c (ig/ref :comp/c)}
              :comp/c {}}]
  (-> config
      (call-subset [:comp/a])
      (call-subset [:comp/b])))

;; Result:
;; => {:comp/a [:a :c]
;; =>  :comp/b [:b :c]
;; =>  :comp/c :c}

Kevin19:09:00

So in this scenario we have the same config / init-keys as before (but now with a println to check if :comp/c gets called once or twice). We create a new function call-subset which only calls the required keys. Afterwards to removes all ig/init-key multimethods of the results keys, and derive those keys from :it/const (same as :duct/const, just return the opts). Finally we merge the new subset config into the original config.

Kevin19:09:38

Uhm sorry, I think I made a mistake somehwere. the result isn't really correct 😕

Kevin19:09:46

Ok I just typo'd my result

Kevin19:09:33

Just copy paste this and try it out in your repl

Kevin19:09:03

(Assuming that I met your requirements 😅 )

Dax Borges20:09:05

looks to be right, now I just need to grok it

Kevin20:09:52

Yeah this really goes into the deep end 🤷

Kevin20:09:48

This would actually be a good addition to integrant-tools :thinking_face:

Dax Borges20:09:09

Music to my ears

Dax Borges20:09:00

I think the one downside to this (though not a huge problem) is needing to define the dep tree all up front

Dax Borges20:09:23

config {:comp/a {:comp-c (ig/ref :comp/c)}
              :comp/c {}}

Kevin20:09:27

That wouldn't be the case?

Kevin20:09:51

You could also merge your "new" config into the old config

Kevin20:09:25

(-> config
    (call-subset [:comp/a])
    (merge {:comp/b {:comp-c (ig/ref :comp/c)}})
    (call-subset [:comp/b]))

Kevin20:09:59

You just need to keep track of your config state, with an atom or something

Kevin20:09:11

(Which makes sense since you want to re-use components)

Kevin20:09:31

Makes sense to me at least

Dax Borges20:09:37

Seems like I need to play with it more. It's actually not me but another member of my team who's offline, I've now spent more time on this than I should have 😬. Really appreciate all the help @kevin.van.rooijen!! We'll test it out and I'll let you know how it goes. Hope you don't mind if we return with more questions 😅

Dax Borges20:09:22

Generally it looks like it should work, just wondering if I'm missing anything between toy code and PoC implementation with our app

Kevin20:09:02

You're welcome, good luck!

Dax Borges20:09:23

Thanks! I'll update you when I learn more!