Fork me on GitHub
#reagent
<
2020-12-30
>
william16:12:55

I'd like some help in debugging an issue with some code I wrote a while ago. I'm using reagent and shadow-cljs , and while the code seems fine to me, if I change anything else in the UI the displayed graph disappears:

(defn workspace-ui []
  (r/with-let [cyto (r/atom nil)
               _    (add-watch
                     app-state
                     :diff
                     (fn [_ _ old-state new-state]
                       (let [[_ new _] (data/diff (:workspace old-state)
                                                  (:workspace new-state))
                             sorted (->> new
                                         judgements->cyto
                                         (sort-by :group #(compare %2 %1))
                                         clj->js)]
                         (.add @cyto sorted)
                         (.run (.layout @cyto #js {:name "cola"
                                                   :infinite true
                                                   :fit false
                                                   :padding 50}))
                         sorted)))]
    (r/create-class
     {:component-did-mount
      (fn [comp]
        (.use cytoscape cola)
        (reset! cyto
                (cytoscape
                 (clj->js {:container (d/dom-node comp)
                           :elements []
                           :style cyto-style}))))
      :reagent-render
      (fn []
        [:div {:style {:width 800 :height 400 :font-size 9}}])})))

william16:12:03

Basically the code is about a widget that uses cytoscape-js to render just the updates of a graph. But the graph disappears when I change even a label in another part of the page, and hit save in emacs

william17:12:31

So I think my mental model of what happens when I save in emacs is a bit lacking

p-himik17:12:01

Can't say anything definitive without an MRE. What is app-state? Also note that your add-watch usage is incorrect - if the component gets unmounted, the watch will not be removed. And lastly, you don't need to mix r/with-let and r/create-class. You can use either but there's no need to use both.

william17:12:24

thank you, do the definitions in a r/with-let clause persist on reloading?

william17:12:49

I think that could be the problem, because I know that on refresh, the component is unmounted

william17:12:05

app-state in this case is just a map that contains a :workspace

p-himik17:12:14

> do the definitions in a r/with-let clause persist on reloading? Not sure. app-state cannot be just a map - it's an atom. Did you use a global def to intern it? If so, try replacing it with defonce.

william17:12:12

you're right, I mean, its definition is:

(defonce app-state (r/atom {:workspace #{}}))

william17:12:44

and I already log that to the dom, so I know that atom is correct, it's just the visualization that goes blank

william17:12:17

@U2FRKM4TW would this work as a way of using only r/with-let?

(defn workspace-ui2 []
  (r/with-let [cyto (r/atom nil)
               _    (add-watch
                     app-state
                     :diff
                     (fn [_ _ old-state new-state]
                       (let [[_ new _] (data/diff (:workspace old-state)
                                                  (:workspace new-state))
                             sorted (->> new
                                         judgements->cyto
                                         (sort-by :group #(compare %2 %1))
                                         clj->js)]
                         (.add @cyto sorted)
                         (js/console.log (.nodes @cyto))
                         (js/console.log @cyto)
                         (.run (.layout @cyto #js {:name "cola"
                                                   :infinite true
                                                   :fit false
                                                   :padding 50}))
                         sorted)))]
    [:div {:ref #(do (.use cytoscape cola)
                     (reset! cyto (cytoscape
                                   (clj->js {:container %
                                             :elements []
                                             :style cyto-style}))))
           :style {:width 800 :height 400 :font-size 9}}]
    ))

william17:12:00

the initialization code is now in the :ref , but I doubt I'm unmounting it cleanly

p-himik17:12:59

You also need the contents of :component-did-mount handler - add it in the same way you use add-watch, right within with-let. Also, add some code with remove-watch. Read the docs of with-let and some Reagent example to understand how to use its finally clause.

p-himik17:12:12

Ah, sorry - I didn't notice the ref. Hmm.

p-himik17:12:48

Refs can be nil - do check for that. Apart from that and the lack of remove-watch, I'd say it looks OK.

william17:12:24

Thank you. Unfortunately even this:

(defn workspace-ui2 []
  (r/with-let [cyto (r/atom nil)
               _    (.use cytoscape cola)
               _    (add-watch
                     app-state
                     :diff
                     (fn [_ _ old-state new-state]
                       (let [[_ new _] (data/diff (:workspace old-state)
                                                  (:workspace new-state))
                             sorted (->> new
                                         judgements->cyto
                                         (sort-by :group #(compare %2 %1))
                                         clj->js)]
                         (.add @cyto sorted)
                         (js/console.log (.nodes @cyto))
                         (js/console.log @cyto)
                         (.run (.layout @cyto #js {:name "cola"
                                                   :infinite true
                                                   :fit false
                                                   :padding 50}))
                         sorted)))]
    [:div {:ref #(reset! cyto (cytoscape
                                   (clj->js {:container %
                                             :elements []
                                             :style cyto-style})))
           :style {:width 800 :height 400 :font-size 9}}]
    (finally (remove-watch app-state :diff)
             (.unmount @cyto))
    ))
still crashes when I try to reload, with errors like:
Cannot read property 'className' of null
I'm trying to determine where this happens now

william17:12:58

I guess my question would be: what's the difference between putting "imperative" code in a :ref or in the bindings of a r/with-let?

p-himik17:12:24

Someone that knows Reagent internals really well could probably answer that question. Alas, I can't.

william17:12:35

thanks nonetheless @U2FRKM4TW 🙂

william21:12:50

I solved all the problems in my previous question. But I still have one doubt: here's a component:

(defn example-component []
  (r/with-let [_ (js/console.log "Mounting")]
    [:h1 "hi"]
    (finally (js/console.log "Unmounting"))))
when the page is loaded, I'll get Mounting, which is expected. But when I save my file in emacs causing a refresh I'll get
Mounting
Unmounting
which is not at all what I would expect! Why does this happen?

p-himik21:12:15

Perhaps the component is remounted and for some reason a new version is mounted before the old one is unmounted.

🙌 3
william21:12:52

That's a possibility for sure, I'm not familiar enough with the tech stack to say. For now, my solution is avoid initialization in the r/with-let block, and instead do all of it in the :ref. This works perfectly:

(defn workspace-ui []
  (r/with-let [cyto (atom nil)]
    [:div {:ref #(when %
                   (.use cytoscape cola)
                   (reset! cyto (cytoscape (clj->js {:container % :style cyto-style})))
                   (.json @cyto @workspace-ui-data)
                   (add-watch app-state :diff
                              (fn [_ _ old-state new-state]
                                (let [[_ new _] (data/diff (:workspace old-state)
                                                           (:workspace new-state))
                                      sorted (->> new
                                                  judgements->cyto
                                                  (sort-by :group >)
                                                  clj->js)]
                                  (.add @cyto sorted)
                                  (.run (.layout @cyto #js {:name "cola"
                                                            :infinite true
                                                            :fit false
                                                            :padding 50})))))

                   )
           :style {:width 800 :height 400 :font-size 9}}]
    (finally
      (reset! workspace-ui-data (.json @cyto))
      (remove-watch app-state :diff)
      (.unmount @cyto))))

p-himik21:12:47

Now you can move all the code in finally inside :ref - it will be called with nil when the component unmounts. Or don't - up to you, it shouldn't change anything.

juhoteperi21:12:34

Only thing I know why "unmount" would be called sometime later, is if you are using v1 and function components. There unmount (which calls with-let finally) is implemented using useEffect hook which doesn't guarantee exit fn is called before component is mounted again.

william21:12:13

@U2FRKM4TW how does that work? Does :ref have the special handling of finally clauses too?

william21:12:31

@U061V0GG2 how do I know if I'm using v1 and function components?

p-himik21:12:35

:ref is called two times - with the ref on mount and with nil on unmount. That's it.

william21:12:48

moreover, I'm using that workspace-ui-data , which is a global atom, to store additional info between refreshes. If I try to define an atom that does that in the r/with_let block, it doesn't work

william21:12:17

@U2FRKM4TW oh, so, instead of when I could use if to discriminate mounting/unmounting

william21:12:24

I think that moving the definition of workspace-ui-data to a r/with-let block doesn't work because it's executed again on reloads, and so it will always put it back to the initial value

william21:12:32

but if so, what's the workaround?

p-himik21:12:06

I just use re-frame for all my state management. :)

william22:12:30

time to take a look at re-frame 😄