tl;dr : the React lifecycle updates
as expected when created as an individual component, but not when piece of a larger component. What can I do?
I’m struggling with a strategic problem, and I hope it doesn’t lose you in the complexity of several moving parts. I successfully built a singular Reagent component that uses the update life cycle timing to receive external data and instrument in its not-react way. This works great! But now I need to include it in my form creation library, which is data-driven so that one master vector with various keywords and maps produces a reagent web form that has users fill in data into appropriate field types. We use this library in many places. I need to extend it to take build this type of field that works singularly; however, this data-driven and reagent approach of sending it all in as one master data structure seems to be messing up the special needs of this one field. Essentially, the :component-did-update
function on the special field is never being run; data updates are only hitting the create and destroy lifecycle events of this single component. I think this is less a code issue than a strategy one; am I allowed to have components with their own lifecycle methods within other components which might have different lifecycle methods? Am I looking at this problem wrong? I have this niggling concern that I might need to delve in to the world of macros that fire before reagent or React in order to pull this off…
Setup: the auto complete component that works on its own
Basically the same code as in my data-driven library, but there it is intended to be amidst a much larger context.
(defn render
;; this is only different in my DD library because it receives only
;; the `:data-subscription` because it is fronted by a function
;; that gives the `data` as a first arg, based on the `:data-subscription`
[data &[ac-args]] ;; REGRET that I have to receive data AND (:data-subscription ac-args) in order for this to update properly
(let [{:keys [:input-id :separators :literals :multi? :throttle-time :fuzzy? :display-name :display-key :placeholder :data-subscription :update-fn]
} ac-args
data-js (if display-key
(map #(DataItem. % display-key) data)
data)
matcher (goog.ui.ac.ArrayMatcher. data-js (not fuzzy?))
renderer (goog.ui.ac.Renderer.)
input-handler (goog.ui.ac.InputHandler. separators literals multi? throttle-time)
auto-complete (goog.ui.ac.AutoComplete. matcher renderer input-handler)
_attach-ac (.attachAutoComplete input-handler auto-complete)]
(r/create-class
{:display-name display-name
:reagent-render
(fn [_data argmap]
[:input {:id input-id :placeholder placeholder}])
:component-did-mount (fn [this]
(let [this-dom (dom/dom-node this)]
(log/info "initial mounting ac")
(log/info ^:meta {:raw-console? true} this-dom)
(.attachInputs input-handler this-dom)
(listen-to-me! auto-complete update-fn)))
:component-did-update (fn [_this] ;; this doesn't happen in the the rich context of a my library form
(log/info "Updating ac mount")
(when-not data-subscription
(throw (ex-info "No :data-subscription given" ac-args)))
(let [data (->> data-subscription deref (map #(DataItem. % display-key)) (apply array))]
(.setRows matcher data)
(listen-to-me! auto-complete update-fn)))
:component-will-unmount (fn [& _]
(log/info "disposing auto-complete")
(.dispose auto-complete))})))
Successfully renders and updates appropriately, alone
Notice that I am passing in the data explicitly (line 2), which causes a Reagent/React data binding, and then also include the un-dereferenced :data-subscription
on line 3 which is required by the :component-will-update
lifecycle function (someone please let me know if there is a less-obnoxious way of getting the binding and the reference).
[ac/render
@(rfc/subscribe [:get-us-states])
{:data-subscription (rfc/subscribe [:get-us-states])
:update-fn (ac/ac-update-fn :us-state)}]
fails: hits the initialize and destroy methods, but not the update one
Obviously this isn’t exactly the same call because I’m not passing in the data explicitly, but I’m not sure that is the problem at hand. This is the syntax heretofore expected by my Reformation library. When I update the value, the :component-did-mount
and the :component-will-unmount
are both fired, but not the :component-will-update
which is where the magic happens when done outside the library.
[:an-autocomplete {:label "Dummy Autocomplete"
:type :autocomplete
:autocomplete-args {:fuzzy? true
:placeholder "Type for suggestions"
:display-key :name
:data-subscription (r/atom [{:name "foo" :other-data 3}])
;; in practice this is an atom external to this function, so the atom can be altered
}}]
;; elsewhere this is parsed by by the following function before
;; it is passed to the `render` function above
(defn autocomplete ;; this wrapper is perhaps not allowing the lifecycle update to occur
"The entry-function for reformation. `opt-map` is expected to have `:autocomplete-args`"
[fn-map-with-path opt-map]
(let [autocomplete-args (:autocomplete-args opt-map)
subscription (:data-subscription autocomplete-args (atom {}))
data @subscription]
(log/info "My args:" autocomplete-args)
(_render data autocomplete-args)))