integrant 2021-09-27

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?

> 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

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

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

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?

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.

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

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

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

Appreciate it!

let me try to get some put together

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

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

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

And this would be the config

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

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

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

adding more to the example .. one min

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

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

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

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

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

but I haven't tried this before

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

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

this part :comp/c {} is wrong

So you have multiple configurations?

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

does that make sense?

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

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

yes, that's a better way of putting it

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

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

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]}

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

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

Ok bear with me on this one 😅

hey, I appreciate any ideas you have!

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

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.

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

Ok I just typo'd my result

Just copy paste this and try it out in your repl

(Assuming that I met your requirements 😅 )

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

Yeah this really goes into the deep end 🤷

This would actually be a good addition to integrant-tools 🤔

Music to my ears

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

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

That wouldn't be the case?

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

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

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

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

Makes sense to me at least

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 😅

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

You're welcome, good luck!

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