reagent

Pablo 2024-05-30T15:45:02.183509Z

Iโ€™m working on an animated input-field that I want to reuse. It uses some refs, so I defined it as a functional component. It contains an input child to extract an input field, but since value is passed in the props map, the input-field component is rerendered on every keystroke.

(defn input
  [{:keys [key type placeholder value on-change error]}]
  (fn [{:keys [key type placeholder value on-change error]}]
    [:input (merge
             {:key         key
              :type        type
              :value       value
              :on-change   on-change
              :placeholder placeholder
              :auto-focus  true}
             (when error
               {:error :true}))]))


(defn input-field
  [{:keys [key type label placeholder value on-change data-info error] :as props}]
  (let [label-ref (react/useRef)
        input-ref (react/useRef)]
    [:<>
     [:> SwitchTransition
      [:> CSSTransition
       {:key              key
        :class-names      :fs-anim-upper
        :node-ref         label-ref
        :add-end-listener (fn [done]
                            (-> label-ref .-current (.addEventListener "transitionend" done false)))}
       [:label {:for       key
                :data-info data-info
                :ref       (fn [el]
                             (println "label-ref" el)
                             (set! (.-current label-ref) el))}
        label]]]
     [:> SwitchTransition
      [:> CSSTransition
       {:key              key
        :class-names      :fs-anim-lower
        :node-ref         input-ref
        :add-end-listener (fn [done]
                            (-> input-ref .-current (.addEventListener "transitionend" done false)))}
       [:div {:class "fs-input" ;; TODO: Remove this div
              :ref   (fn [el]
                       (println "input-ref" el)
                       (set! (.-current input-ref) el))}
        [input (select-keys props [:key :type :placeholder :value :on-change :error])]]]]]))

(defn form
  []
  (r/with-let [state (r/atom ...)]
    (let [current-field (get-in @state ...)
          current-value (get-in @state ...)
          input-data    {:value current-value
                         ...}]
      ...
      [:f> input-field input-data]
      ...)))
How can I pass the value to the input component without causing every child of input-field to rerender on each keystroke?

p-himik 2024-05-30T16:04:19.674959Z

You can pass a ratom with the value. To keep the input itself generic, you can make it deref the value only if it's deref'able. re-com.utils has deref-or-value if you're curious about impl.

Pablo 2024-05-30T16:24:52.257549Z

I imagine I will always have to do the deref. I wanted to expose an interface like Semantic or MUI, but since reagent rerenders when changing props, I think it wonโ€™t be possible.

p-himik 2024-05-30T17:00:32.267809Z

I don't see how passing the props around makes generalized components impossible. If you pass ratoms around, the parent component will not be re-rendered if the value inside the ratom changes. Well, unless that component also deref's the ratom.

Pablo 2024-05-30T18:04:18.392319Z

Do you mean something like this?

(defn input
  [state {:keys [key type placeholder]}]
  (fn [state {:keys [key type placeholder]}]
    (let [field-data (get-in @state [:data key])]
      [:input (merge
               {:key         key
                :type        type
                :value       (:value field-data)
                :on-change   (fn [e]
                               (swap! state assoc-in [:data key] {:value (-> e .-target .-value)})
                               (r/flush))
                :placeholder placeholder
                :auto-focus  true}
               (when (:error field-data)
                 {:error :true}))])))


(defn input-field
  [state {:keys [key type label placeholder data-info] :as props}]
  (let [label-ref (react/useRef)
        input-ref (react/useRef)]
    ...
    [input state (select-keys props [:key :type :placeholder])]))

p-himik 2024-05-30T18:30:48.122629Z

No, here you're passing all of the state into input, that's not great. More like something like this (haven't tested):

(defn input [{:keys [value on-change]}]
  [:input {:value (deref-or-value value)
           :on-change on-change}])

(defn input-wrapper [input-props]
  [:div {:class :wrapper}
   [input input-props]])

(defn form []
  ;; This component doesn't deref the state, so it shouldn't be re-rendered when it changes internally.
  (let [state (r/atom {:foo "", :bar ""})]
    [:div
     ;; Cursors are similar to ratoms. You can also change them directly.
     [input-wrapper {:value (r/cursor state [:foo])
                     :on-change #(swap! state assoc :foo %)}]
     [input-wrapper {:value (r/cursor state [:bar])
                     :on-change #(swap! state assoc :bar %)}]]))
                     

๐Ÿ‘€ 1
Pablo 2024-05-31T14:51:25.184859Z

Thank you so much. I wasnโ€™t aware of cursors before. As you mentioned, now my components re-render only when they should.

๐Ÿ‘ 1
p-himik 2024-05-31T15:06:01.747629Z

There are also reactions. And a bunch of other useful things. :)

๐Ÿ‘๐Ÿป 1