Fork me on GitHub
#re-frame
<
2022-03-12
>
fadrian12:03:17

I'm having a problem constructing more complex re-frame components out of simpler ones. To save space on the main thread, code will be in the thread post.

fadrian12:03:39

Here is my code for primitive components:

(defn integer
  [id attrs]
  ; Set up events and subscription handlers.
  (set-up-reactivity :on-integer-change :integer-state)
  (fn
    [id attrs]
    ; Merge attrs with pre-defined attrs, overriding if necessary.
    (let [comp (merge-component-attrs
                [input-text
                 :src (at)
                 :model  (@(rf/subscribe [:integer-state id]))
                 :on-change #(rf/dispatch [:on-integer-change id
                                           (if (nil? %) nil (js/parseInt %))])
                 :width "150px"
                 :change-on-blur? true]
                attrs)]
      [:div.integer comp])))
To render this component one uses:
[integer (gensym) {...}]
where the gensym is a sub-key under which the event is stored in the db. So far primitives like this are all working. The problem comes when I want to bundle two primitive components into a higher-level components. Here was my initial attempt:
(defn integer-interval
  [id]
   (let [low-int-id (gensym)
        high-int-id (gensym)]
    (fn
      [id]
      (let [model @(rf/subscribe [:integer-interval-selector-state id])
            low-val (:low model)
            high-val (:high model)]
        (rf/dispatch [:on-integer-change low-int-id low-val])
        (rf/dispatch [:on-integer-change high-int-id high-val])
        [v-box
         :children
         [[integer low-int-id]
          [integer high-int-id]]]))))
The question is how I maintain state between the lower-level integer components and the overall model-state stored in the database as the map {:low <low-val>, :high <high-val>} under [:integer-interval-state id]. I have made several attempts to reconcile the state held in the integers with the state held in the upstream model, but so far, I'm not coming up with a good pattern that sets the high-level state without knowing the id of the high-level component to update when the integer primitives change.

p-himik12:03:48

The downside of splitting your post into multiple ones without typing them in some text editor in advance is that people can't figure out whether you have said what you wanted or are still in progress. So far, you have a description of the intent and of the first attempt, so I guess you're still writing about what went wrong?

fadrian12:03:35

Yes. It's still being edited. Is there a way to change slack's behavior so that enter doesn't also send?

p-himik12:03:02

Shift+Enter to add a new line. And there's an Advanced setting in addition to that. But I would strongly recommend using an external editor. :) Slack editing can result is sync issues - on occasion, the client can remove words or whole sentences.

fadrian12:03:36

I'm done editing. I think the question makes sense now.

p-himik12:03:51

> a good pattern that sets the high-level state without knowing the id of the high-level component It has been discussed at length at https://github.com/day8/re-frame/issues/137 There are many approaches - not caring about it, passing paths instead of IDs, passing and composing getters/setters, using React contexts,... As of now there's no perfect solution, so the ultimate decision is definitely on your shoulders. With that being said, your code tries hard to go against the grain of the whole re-frame model. I think I've already mentioned it before, but still: • Don't use side effects, especially things like dispatch (and set-up-reactivity looks very suspicious), in the view functions themselves. Instead, use them in events that setup those views. In re-frame, data drives the views - not the other way around • Don't use (gensym) or anything like that in views - use such things in events, just like in the previous point Those might look unimportant right now but they will save your bacon in a more complex application.

fadrian13:03:27

Thanks. I'll try to digest this. Set-up-reactivity is a wrapper around database state and event and subscription declarations that ensure that the map that stores the components' state has been created, that the event handler has been declared, and that the subscription for the data has been declared. I could break all of these apart and put the calls for each of my components into my db.cljs, events.cljs and subscriptions.cljs files, but it seems like this would introduce a lot of redundant code. But I'm willing to try this. I'm also not sure how I would ensure that these tasks get completed before the rendering code that needed them is run. Any ideas about ordering in shadow-cljs re-frame builds?

fadrian13:03:26

Scratch that last question. I was having a brain fart.

p-himik13:03:52

Don't register subs and events handlers dynamicly, except for when it's done in a very narrow scope - like in a top-level doseq or something like that. Definitely don't register them in a view function. You don't have to register them in a separate files though - it can all be in the same one. > I'm also not sure how I would ensure that these tasks get completed before the rendering code that needed them is run. Registering subs/events/whatever is done when the namespace is required - basically, when main.js is evaluated. You don't need to postpone this step, there's 0 reason to do it. And regarding pre-loading all the required state - that's exactly what the relevant events are for.

fadrian13:03:04

Thanks again. I guess a nice thing about clojure code is that there's so much less of it to mess around with when you make structural changes. The primitives all work as expected (modulo the set-up-reactivity calls, which really won't take long to move around). So I only have three complex components that have to reconcile state for which I think I'll need to use custom events. I'd assume that the event handler is the correct place to put the (gensym)'s that create the state for the low-level components and the reconciliation logic. Any idea how to get the symbols from there back to the render function so they it can be hooked up properly? I guess I could store those as part of the model state, too, but that seems a bit clunky. Even so, I can't think of anything better.

p-himik13:03:45

> the event handler is the correct place to put the (gensym)'s that create the state for the low-level components and the reconciliation logic Either there, or in the code that dispatches that event - maybe another event handler (via an effect), maybe an effect handler, maybe a JS event handler. > Any idea how to get the symbols from there back to the render function so they it can be hooked up properly? By "symbols" I assume you mean IDs, because the fact that you're using gensym is non-consequential here as what you really want is an ID. Just put them in a relevant place in the app-db.

p-himik13:03:53

Whatever you can possibly consider your app's state, put it in app-db - that's exactly what it's for. There are rare exceptions that might make more sense being used in a component's local state - e.g. whether a dropdown component is open or not. It makes especially more sense in reusable components.

fadrian13:03:31

That's what I figured. Thanks for your help. Back to coding...

p-himik13:03:52

I'd also recommend going through the ToDo example re-frame app, and reading through the re-frame documentation once more. It describes everything that I've already said, and more. Good luck!