Fork me on GitHub
#reagent
<
2022-11-06
>
Sam Ritchie22:11:33

hey all… I am having a problem where an effect seems to be firing on every render instead of only when my specified property updates. Here is an example:

(def Demo
  (r/adapt-react-class
   (react/forwardRef
    (fn [js-props ref]
      (react/useEffect
       (fn []
         (js/console.log "resetting options!"))
       #js [(aget js-props "options")])
      (r/as-element
       [:textarea
        {:value (aget js-props "value")
         :ref ref
         :on-change (aget js-props "onChange")}])))))
If I run
(reagent/with-let [state (reagent/atom "face")]
   [Demo {:options {}
          :value @state
          :on-change #(reset! state (.. % -target -value))}])
I see “resetting options!” fire on every change to the textarea. If instead I do NOT use forwardRef then there is no problem:
(defn Demo2 [{:keys [value on-change options]}]
  (react/useEffect
   (fn mount []
     (js/console.log "resetting options!"))
   #js [options])
  [:<>
   [:pre value]
   [:textarea
    {:value value
     :on-change on-change}]])
The same snippet above with Demo2 does not trigger the hook. does this have to do with JS object identity vs value?

p-himik22:11:40

Unrelated to your question, but you should avoid using aget to get properties out of a JS object. Instead, just use plain interop, as in (.-value ^js js-props).

Sam Ritchie22:11:01

@U2FRKM4TW I’m reading about this and the answer seems to be that because I am dealing with js object equality, I need to use useRef and convert back to clj data structures internally to do this: https://www.benmvp.com/blog/object-array-dependencies-react-useEffect-hook/#option-4---do-it-yourself

p-himik22:11:43

Maybe! But I don't really have a clue, sorry. I've never really used React hooks. And if I ever start using them heavily enough, I'd probably look at Helix with a potential of using it alongside with Reagent.

Sam Ritchie22:11:37

(def Demo
  (r/adapt-react-class
   (react/forwardRef
    (fn [js-props ref]
      (let [opts-ref (react/useRef (.-options js-props))]
        (when (not= (js->clj opts-ref.current)
                    (js->clj (.-options js-props)))
          (set! (.-current opts-ref)
                (.-options js-props)))

        (react/useEffect
         (fn mount []
           (js/console.log "resetting options!"))
         #js [opts-ref.current])

        (js/console.log (pr-str js-props))
        (r/as-element
         [:textarea
          {:value (aget js-props "value")
           :ref ref
           :on-change (aget js-props "onChange")}]))))))

Sam Ritchie22:11:11

@U2FRKM4TW makes sense! basically this works great with JUST cljs, except because I am ALSO using forwardRef I’m stuffed back into JS land

lilactown04:11:32

React uses reference equality to compare objects passed into the effects deps array

lilactown04:11:04

so even tho {} is the same value each render it's creating a new object with a new reference

lilactown04:11:13

and thus the effect fires

Sam Ritchie04:11:18

yup, had to drill that back into my head

p-himik05:11:58

@U4YGF4NGM But {} is always evaluated to the same object - the PersistentArrayMap.EMPTY singleton. Where does a new reference come from?

lilactown04:11:46

I don't know if that's true in development. if so then we're back to the drawing board

Sam Ritchie05:11:41

@U2FRKM4TW it comes from the conversion to js in react/forwardRef before it calls my function with props and ref

Sam Ritchie05:11:34

But maybe I am missing something, will stare in the AM