Strategy: data-driven Reagent/React component lifecycles?

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)))

I’m pretty sure this is a case of a rookie mistake described in this docs section: reagent/CreatingReagentComponents.md at master · reagent-project/reagent · GitHub

While that section talks about form-2 components and yours is form-3, the same rules still apply. In order for your lifecycle functions to get the latest value of any of the props, you have to get those props from the function’s arguments.
:component-did-mount should have the same props as the ones that were used to instantiate the component but it’s hard for me to say whether it’s always the case (which becomes even harder once Reagent starts using React 18 features), so I’d use one of (r/props this), (r/argv this), or (r/children this) in there.
Same for :component-did-update.

An unrelated advice - don’t use {:keys [:a]}, use {:keys [a]} instead. Shorter, more readable, much more common.

Thanks for the response. I’m definitely not above rookie mistakes, but funny that they would still be creeping around after I’ve been making constant use of Reagent for years, since at least 2017…

Perhaps it’s a valid criticism of Reagent/React that “magic” happens incident to putting args in the call of the function, and I can’t find any other way to instantiate a data binding. It would be nice if there was something more explicit; as it is, if you put enough wrappers around a call (maybe even just one wrapper?) the data doesn’t get bound for updates? Ugh.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.