2
votes

Consider the following reagent component. It uses a ref function, which updates a local state atom, based on the real size of a span element. This is done in order to re-render the component displaying its own size

(defn show-my-size-comp []
  (let [size (r/atom nil)]
    (fn []
      (.log js/console "log!")
      [:div
       [:span {:ref (fn [el]
                      (when el (reset! size (get-real-size el))))} 
        "Hello, my size is:" ]
       [:span (prn-str @size)]])))

If the implementation of get-real-size returns a vector, the log message is printed constantly, meaning the component unnecessarily being re-rendered all the time. If it returns just a number or a string, the log appears only twice - as intended in this scenario.

What's the reason for this? Is it maybe that updating an clojure script atom with a new vector (containing the same values though) internally means putting another JavaScript object there, thus changing the atom? Whereas putting a value produces no observable change? Just speculation...*

Anyways - for the real use case, saving the size of the span in a vector would certainly better.. Are there ways to achieve this?

I cam across this, when trying to enhance the answer given in this question.


* since in JS: ({} === {}) // false

3
I'm not near a setup where I can test it, but try logging the computed size in get-real-size. My guess is it's constantly changing. That'd be because every time it's gets it's own size and displays a different value, the width changes so it re-renders, gets a new ref, with a new size, which has a different width.... - Walton Hoops
nope, I guessed so too. It's the same when the function is a dummy: (defn get-real-size [_] [0 0]) - Anton Harald

3 Answers

6
votes

I think I have an answer for why vector behaves differently from string/number. Reagent counts a reagent atom as "changed" (and thus updates a component that depends on it) when identical? returns false between the old and the new values. See the subhead "changed?" in this tutorial:

For ratoms, identical? is used (on the value inside the ratom) to determine if a new value has changed with regard to an old value.

However, it turns out that identical? behaves differently for vectors and for strings/ints. If you fire up either a clj or a cljs repl, you'll see that:

(identical? 1 1)
;; true 
(identical? "a" "a")
;; true 
(identical? [1] [1])
;; false
(identical? ["a"] ["a"])
;; false

If you look at what identical? does here, you'll see that it tests if its arguments are the same object. I think the underlying internal data representation is such that, in clojure, "a" is always the same object as itself, whereas two vectors containing the same value are not the same object as one another.

Confirmation: with ordinary rather than reagent atoms, we can see that string identity is preserved across atom resets, while vector identity is not.

(def a1 (atom "a"))
(let [aa @a1] (reset! a1 "a") (identical? aa @a1))
;; true
(def a2 (atom ["a"]))
(let [aa @a2] (reset! a2 ["a"]) (identical? aa @a2))
;; false
3
votes

You can work around the problem with a not= check:

                (fn [el]
                  (when el
                    (let [s (get-real-size el)]
                      (when (not= s @size)
                        (reset! size s)))))

I'm not sure what the reason is for why vectors should differ from other values.

0
votes

Its rerendering like it is supposed to based on how its written. You are derefing the atom in the same function as you are resetting it. I always keep these separate.

(defn span-size [size]
  [:span (prn-str @size)])

(defn show-my-size-comp []
  (let [size (r/atom nil)]
    (fn []
      (.log js/console "log!")
      [:div
       [:span {:ref (fn [el]
                      (when el (reset! size (get-real-size el))))}
        "Hello, my size is:"]
       [span-size]])))