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?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.
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.
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.
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])]))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 %)}]]))
Thank you so much. I wasnโt aware of cursors before. As you mentioned, now my components re-render only when they should.
There are also reactions. And a bunch of other useful things. :)