This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-02-20
Channels
- # announcements (1)
- # architecture (14)
- # asami (21)
- # babashka (1)
- # beginners (44)
- # biff (6)
- # calva (24)
- # clojure (16)
- # clojure-europe (12)
- # clojurescript (32)
- # cursive (23)
- # datascript (5)
- # honeysql (8)
- # hyperfiddle (1)
- # malli (1)
- # nextjournal (34)
- # nrepl (4)
- # off-topic (64)
- # re-frame (12)
- # reagent (1)
- # releases (2)
- # reveal (41)
- # shadow-cljs (137)
- # spacemacs (4)
- # xtdb (5)
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?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
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.
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).
Thanks for the tips. They made the code shorter, plus it has the advantage of working now. Thanks again.
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.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.
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.
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.