Fork me on GitHub
#re-frame
<
2024-04-17
>
lwhorton16:04:54

i've been exploring tradeoffs of structuring reusable components in re-frame, and would love to share what i've come up with:

(reg-event-fx
 :parent-component/setup-reusable
 (fn [{:keys [db]}]
   (let [id-a :task-list-a
         id-b :task-list-b
         tasks-a (get-in db ...)
         tasks-b (get-in db ...)]
     {:fx [[:dispatch [:reusable-feature/initialize {:tasks tasks-a
                                                     :id id-a}]]
           [:dispatch [:reusable-feature/initialize {:tasks tasks-b
                                                     :id id-b}]]]})))

(reg-event-db
 :reusable-feature/initialize
 (fn [db [{:keys [id tasks]}]]
   (assert id "reusable component requires an id")
   (assoc-in db [:reusable-feature/state id] {:checked-tasks #{}
                                              ;; note we dont copy tasks here!
                                              :other-initial-state ...})))


(defn reusable-kiwi [{:keys [id <-tasks]}] 
  (let [feature-tasks @(rf/sub [:reusable-feature/tasks-with-selection id <-tasks])
        inspect-task-event @(rf/sub [:reusable-feature/inspect-task-event id])
        prv-event @(rf/sub [:reusable-feature/prv-event id])
        nxt-event @(rf/sub [:reusable-feature/nxt-event id])]
    ...))

(defn parent-component []
  (let [;; note these are not deref'd subs!
        <-a-tasks (rf/sub ...)
        <-b-tasks (rf/sub ...)
        ;; but this is a normal sub
        other-parent-state @(rf/sub ...)]
    [:div
     ;; we blend deref'd subs with signals
     (when (:some-condition other-parent-state)
       [reusable-kiwi {:id :task-list-a :<-tasks <-a-tasks}])

     [reusable-kiwi {:id :task-list-b :<-tasks <-b-tasks}]]))
in this strategy, we blend static state on reusable-feature/initialize with reactive state in a component's view through :<-* provided signals. are the tradeoffs worth it? i think so. much like reagent form-1 and form-2 components, this strategy provides the option to render a component as a purely reactive fn OR a stateful component that holds onto a copy of its own local component state. we trade a slightly more verbose (cumbersome?) interface for optionality and flexibility, which i think is a great tradeoff.

p-himik16:04:19

It would be nice to see an unabridged, self-sufficient example.

lwhorton16:04:22

i'll work on that and get back to you. the larger context here is that i've been waffling back and forth. i haven't been able to settle between 3-4 different strategies for composable reuse. after evaluating their tradeoffs, i think the 'best' idea i've landed on is: 1. send events to initialize/deinitialize a reusable component. in the init, you must provide a unique id, but you can optionally provide any other relevant data that you deem non-reactive. this non-reactive data is a 'snapshot'. 2. a parent can consume a reusable component by sending the initialize event, then passing to the component's view the unique id established via the event, and any other :<-signals that you deem reactive 3. the reusable component has exclusively self-contained subscriptions (indexed by the provided id, so we can have multiple copies) for managing its own reactive state. it knows nothing about the broader subscriptions available. any reactive signals provided by a consumer are utilized to compute more self-contained subscriptions. 4. when you're 'done' with a reusable component, its polite to clean-up by sending a deinitialize event with the original unique id.

lwhorton16:04:21

i'm learning it's quite tricky to design a good reusable/composable interface that doesnt end up grossly complicated or verbose or anemic or overly coupled... but i guess that's true for most interfaces, re-frame or not

john17:04:13

seems like, for maximum reusability points, the id of a component should only have to be referred to statically once, where it's defined. Everything else under the reusable abstraction should see the id as an opaque variable. So seeing :task-list-a in :parent-component/setup-reusable explicitly seems a little sus

john17:04:02

Oh, :parent-component/setup-reusable belongs statically to parent-component, I see

john17:04:18

Yeah, so reusable-kiwi looks reasonably abstracted, getting state path management for free from the id

lwhorton17:04:50

this reusability is kind of a fun problem that i'm not sure anyone has 'solved' well in a frontend framework. https://github.com/day8/re-frame/issues/137 is people trying to implement some generic reusable solution in reframe going back to 2015, for example

john17:04:11

Yeah, I think reusable state abstractions are somewhat orthogonal to reusable concrete component composition

john17:04:00

Giving everything a unique id (at least if it's stateful) makes everything easier all around. Gensym an id in the framework automatically if it's not provided, if you have to

john17:04:42

Then it can automatically have a slot in the db, whether you use it or not

💯 1
Kimo10:04:56

This is a really tricky discussion. I'm slowly reviewing everything that's been said and done. Personally I think @U9CM3SLP3 made some of the soundest arguments. In practice, I've used patterns similar to OP. And in some cases at day8, it's been enough to simply use https://github.com/day8/re-frame/blob/5bd82b3d6625af1fac2b21ba0cd5bab448e44ffe/src/re_frame/std_interceptors.cljc#L158, giving us a pattern of separation without actually putting any values or concerns outside of the app-db.

john13:05:32

Yeah, I think a framework can abstract away component state placefulness for re-frame 90% of the time

john13:05:38

The framework just has to gen a unique id for each statefull component automatically if the user doesn't provide an id

john13:05:56

Placefulness should be an automatic thing by the reusable framework unless overriden by the user