Fork me on GitHub
#re-frame
<
2022-02-20
>
fadrian13:02:27

I am attempting to build a reusable re-frame component containing re-com components:

(defn radio-button-group
  "Build a radio button group with the given id having
   cheices as labels/values and the given wrapper (one of h-box or v-box)."
  [choices wrapper id]
  [wrapper
   :src (rc/at)
   :size "none"
   :gap "20px"
   :children
   (mapv
    (fn [choice]
      (vector radio-button
              :src (rc/at)
              :label choice
              :value choice
              :model @(rf/subscribe [:radio-button-group-state id])
              :on-change #(rf/dispatch [:on-radio-button-group-change id %]))) choices)])

(defn make-radio-button-group
  [choices wrapper]
    (let [rb-grp-id (gensym)
          _ (rf/dispatch-sync [:initialize-radio-button-group (first choices) rb-grp-id])]
      (radio-button-group choices wrapper rb-grp-id)))

(defn container
  []
  (make-radio-button-group ["A" "B" "C"] h-box))
The initializer in make-radio-button group looks like this:
(rf/reg-event-db
 :initialize-radio-button-group
   (fn [db [_ init id]]
     (js/alert (str "Initializing radio button group " id " to " init))
     (let [btn-state (get-in db [:radio-button-group-state id])]
       (js/alert "Current button group state is " btn-state)
       (let [new-db (when (nil? btn-state) (assoc-in db [:radio-button-group-state id] (r/atom init)))]
         (js/alert (str "Current db state is " new-db))
         new-db))))
(which would be a lot simpler without the debugging stuff). In addition, I have a change handler for the radio button groups:
(rf/reg-event-db
 :on-radio-button-group-change
 (fn [db [_ id new-value]]
   (swap! (get-in db [:radio-button-group-state id]) (fn [_] new-value))
   (js/alert (str "Now db value is " db))
   db))
as well as a subscription function to the state of the radio button groups:
(rf/reg-sub
 :radio-button-group-state
 (fn [db [_ id]]
   @(get-in db [:radio-button-group-state id])))
When I render the radio button group for the first time, everything works fine - the initial state is set to the first element of the choices vector and the button group displays fine. However, when I select a radio button in the group, the value changes, and the button group needs to re-render, which creates a new button group. The new button group sets its initial state, which re-creates and re-renders a new button group, and so it goes... Is there any way to stop re-frame from re-rendering/creating a new radio button group and have it keep the original one?

p-himik13:02:16

Issues in your code: • Mostly a cosmetic one - don't use (vector ...), instead just use the vector literal, [...] • Don't call gensym on each render • Don't use dispatch or dispatch-sync in view functions • Don't use () for Reagent components - use [] instead, the difference is documented somewhere • Don't store atoms in app-db - store the values themselves. There must be no swap! or @ in your subscription/event handlers

fadrian14:02:59

Agreed with all of your recommendations and I'll fix the code, but as far as the second goes, where can I put the gensym so it gets called before the button group is rendered, but so it doesn't get re-invoked when something higher up the render tree gets re-rendered (other than shoving it all the way up into the main function and then passing it down through the intermediate render functions)? Basically, I want the id to be created when the group is created, but later I don't want to re-create the group each time the container is re-rendered.

p-himik14:02:47

Rendering should be a side-effect of your data change - not the other way around. In other words, the fact of something being rendered should not affect anything. There are exceptions, of course (e.g. when you interact with a complex JS component), but this is not such a case. Things are rendered because of data changes - so use gensym or anything like that in things that change the data, namely in event handlers or things that feed them data, like effects or JS event handlers (`:on-click` for example).

fadrian14:02:24

I'll try that.

fadrian14:02:25

Thanks for the tips. They made the code shorter, plus it has the advantage of working now. Thanks again.

👍 1
fadrian15:02:01

I was wrong - things are not quite working as I had thought. Here's my new code:

(defn radio-button-group
  "Build a radio button group with the given id having
   cheices as labels/values and the given wrapper (one of h-box or v-box)."
  [choices wrapper id]
  [wrapper
   :src (rc/at)
   :size "none"
   :gap "20px"
   :children
   (mapv
    (fn [choice]
      [radio-button
       :src (rc/at)
       :label choice
       :value choice
       :model @(rf/subscribe [:radio-button-group-state id])
       :on-change #(do (js/alert %)(rf/dispatch [:on-radio-button-group-change id %]))]) choices)])

(defn container
  []
  (js/alert "Rendering container")
  (let [rbg-id (gensym)]
    [v-box
     :children
     [[radio-button-group ["A" "B" "C"] h-box rbg-id]
      [labeled-component "Radio button group value" [:div @(rf/subscribe [:radio-button-group-state rbg-id])]]]]))
I know you said to move the gensym into places for data changes. However, initially, I want to render container and the radio-button-group with the id which the gensym provides needs to be created before that. There's not a data change that triggers that - just a workflow. Or maybe I wasn't understanding what you said.

p-himik15:02:42

You can put that (gensym) some place where you initialize re-frame's app-db and grab the ID from there. Alternatively, you can switch from a form-1 component to a form-2 one or to reagent.core/with-let to keep the ID stable between re-renderers. But it won't save you if container is not merely re-rendered but re-mounted, so I'd advice against that approach unless you absolutely certain about what you're doing and why.

fadrian15:02:15

I turned the form-1 into a form-2 component. I had forgotten about them. It's now working as I expected. Thanks again for your help.

👍 1
paulbutcher22:02:03

Any suggestions for how to debug the following?

re-frame: ":fx" effect expects a seq, but was given  null
Clearly I have an event handler that’s setting :fx to null. But how do I find out which one? The call stack isn’t any help, sadly.

p-himik22:02:08

Yeah, the callstack will show re-frame's internals. You can put a conditional breakpoint in your browser right where events are scheduled, in those very internals. Alternatively, you can register a temporary global interceptor that would check for :fx being nil.

paulbutcher22:02:13

That makes sense. Thanks 👍

👍 1