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}}])})))
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
So I think my mental model of what happens when I save in emacs is a bit lacking
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.
thank you, do the definitions in a r/with-let
clause persist on reloading?
I think that could be the problem, because I know that on refresh, the component is unmounted
app-state
in this case is just a map that contains a :workspace
> 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
.
you're right, I mean, its definition is:
(defonce app-state (r/atom {:workspace #{}}))
and I already log that to the dom, so I know that atom is correct, it's just the visualization that goes blank
@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}}]
))
the initialization code is now in the :ref
, but I doubt I'm unmounting it cleanly
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.
Ah, sorry - I didn't notice the ref. Hmm.
Refs can be nil - do check for that. Apart from that and the lack of remove-watch
, I'd say it looks OK.
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 nowI 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
?
Someone that knows Reagent internals really well could probably answer that question. Alas, I can't.
thanks nonetheless @p-himik 🙂
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?Perhaps the component is remounted and for some reason a new version is mounted before the old one is unmounted.
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))))
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.
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.
@p-himik how does that work? Does :ref
have the special handling of finally
clauses too?
@juhoteperi how do I know if I'm using v1 and function components?
:ref
is called two times - with the ref on mount and with nil on unmount. That's it.
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
@p-himik oh, so, instead of when
I could use if
to discriminate mounting/unmounting
Yep.
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
but if so, what's the workaround?
I just use re-frame for all my state management. :)
time to take a look at re-frame 😄