reagent

A minimalistic ClojureScript interface to React.js http://reagent-project.github.io/
william 2020-12-30T16:52:55.444600Z

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

william 2020-12-30T16:54:03.444700Z

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

william 2020-12-30T17:04:31.444900Z

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

p-himik 2020-12-30T17:10:01.445100Z

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.

william 2020-12-30T17:11:24.445300Z

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

william 2020-12-30T17:11:49.445500Z

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

william 2020-12-30T17:13:05.445700Z

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

p-himik 2020-12-30T17:14:14.446Z

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

william 2020-12-30T17:15:12.446200Z

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

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

william 2020-12-30T17:15:44.446400Z

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

william 2020-12-30T17:22:17.446600Z

@p-himik 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}}]
    ))

william 2020-12-30T17:23:00.446800Z

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

p-himik 2020-12-30T17:23:59.447Z

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-himik 2020-12-30T17:24:12.447200Z

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

p-himik 2020-12-30T17:24:48.447400Z

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

william 2020-12-30T17:35:24.447600Z

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

william 2020-12-30T17:35:58.447800Z

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-himik 2020-12-30T17:48:24.448Z

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

william 2020-12-30T17:55:35.448200Z

thanks nonetheless @p-himik 🙂

william 2020-12-30T21:02:50.450200Z

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-himik 2020-12-30T21:22:15.450500Z

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

🙌 1
william 2020-12-30T21:24:52.450700Z

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-himik 2020-12-30T21:29:47.451Z

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.

juhoteperi 2020-12-30T21:32:34.451200Z

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.

william 2020-12-30T21:37:13.451400Z

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

william 2020-12-30T21:37:31.451600Z

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

p-himik 2020-12-30T21:38:35.451800Z

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

william 2020-12-30T21:38:48.452Z

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

william 2020-12-30T21:39:17.452200Z

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

p-himik 2020-12-30T21:39:49.452400Z

Yep.

william 2020-12-30T21:41:24.452700Z

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

william 2020-12-30T21:41:32.452900Z

but if so, what's the workaround?

p-himik 2020-12-30T21:58:06.453100Z

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

william 2020-12-30T22:05:30.453300Z

time to take a look at re-frame 😄