Fork me on GitHub
#reagent
<
2022-03-30
>
Sam Ritchie01:03:50

Hey all! Question for you - there is a library of react components that I want to wrap up for a nicer cljs experience; some of the components take js objects and vectors, and I would rather the user be able to pass cljs data structures. is this a common thing to do? any good libraries to copy? The ideal thing would be if I could extend the keyword parsing that allows for [:div …] and friends, but I don’t think that’s possible.

Sam Ritchie01:03:46

So it is between having the user write

[mbr/Mathbox
 {:options
  (clj->js
   {:plugins ["core" "controls" "cursor"]
    :controls {:klass mbr/orbit}
    :camera {}})
  :style (clj->js {:height "400px" :width "100%"})
  :initialCameraPosition mbr/init-cam}
 [mbr/Cartesian
  {}
  [mbr/Grid {:axes (:axes value)}]]]
without the clj->js, or
[(r/adapt-react-class MB/Mathbox)
   {:options (options)
    :style #js {:height "400px" :width "100%"}
    :initialCameraPosition init-cam}
   [(r/adapt-react-class MB/Cartesian)
    {}
    [(r/adapt-react-class MB/Grid) m]]]

Sam Ritchie03:03:59

ah! I didn’t realize the clj->js is already handled!

juhoteperi06:03:38

You can also use [:> MB/Mathbox ...] as a shortcut to adapt-react-class inside hiccup forms

Sam Ritchie17:03:17

that is great. I guess the nice thing about doing a wrapper, or using adapt-react-class, is that if I want to add higher-level components to the system then they can all use [this [syntax …]] vs some of the components being special-cased as :> in the user’s brain

Sam Ritchie17:03:53

(into {} (map (fn [[k v]]
                [(symbol k)
                 (r/adapt-react-class v)]))
      (.entries js/Object MB))

Sam Ritchie18:03:02

Let’s say I have a component that takes a single argument f of some quoted source code, like (fn [x] (* x x)) . What I actually need to use inside the component is (eval f), the actual function; so I would like to bind f' to (eval f) , and only call eval any time f changes. I would THINK I want a form-2 like this:

(defn Fn [f]
  (let [f' (xc/sci-eval f)]
    (fn [f]
      [...])))
but the docs tell me it’s a “rookie mistake” to not include f again in the inner function’s args. but I don’t want the inner function’s args! or maybe this is a case where I have to manually handle this? like
(defn Fn [f]
  (let [f' (r/atom [f (xc/sci-eval f)])]
    (fn [f]
      (swap! f' (fn [pair]
                  (if (= f (first pair))
                    pair
                    [f (xc/sci-eval f)])))
      ,,,)))

Sam Ritchie18:03:37

this must be a common pattern, like calling “fmap” or “map” on a reactive value

p-himik18:03:46

Ways to handle this: • With :component-did-update, but it's verbose • With the useEffect hook, but it might be cumbersome to use • With a pair, like you describe - either in single ratom or in two ratoms • With a memoized function with cache size of 1

borkdude18:03:12

I think I would also memoize the eval function on the quoted expression

p-himik18:03:29

Also, I think that just a form-1 component will not call xc/sci-eval too many times. When [Fn '(fn [x] (* x x))] is encountered, it will result in a specific Fn-based React component and then a React element with that '(fn ...) in the props, and that will create an instance of the component. That instance should not change as long as that Hiccup vector stays the same and the parent is not re-mounted.

☝️ 1
p-himik18:03:57

It becomes more of a problem when your component receives more than one argument and those arguments can change independently.

Sam Ritchie18:03:20

that is interesting, I think that is tied to a “bug” I am seeing in clerk. it seems to be the case (correct me if I’m wrong @U2FRKM4TW that if I pass a bunch of props in a map to some function f, then if any of them change, the entire tree returned by f will re-render?

Sam Ritchie18:03:23

or, alternatively, if f returns value [:div [a arg1] [b arg2] [c arg3]] then only the specific components whose arguments change should re-render?

Sam Ritchie18:03:36

@U04V15CAJ the trouble is that I want to call f' in an animation loop, so I want to make sure that even if it’s memoized we are not calling xc/sci-eval more often I need to. or maybe it doesn’t really matter, the map lookup will be so cheap for the memo cache that it’s fine

Sam Ritchie18:03:58

but ALSO memoize is a good idea

Sam Ritchie18:03:21

@U2FRKM4TW :component-did-update actually seems like the most clean way to do this

p-himik18:03:27

> if I pass a bunch of props in a map to some function f, then if any of them change, the entire tree returned by f will re-render? That is true - even if you don't use a prop, the Hiccup vector is different. > if f returns value [:div [a arg1] [b arg2] [c arg3]] then only the specific components whose arguments change should re-render? If you use something like [f {:arg1 1 ...}] and then :arg1 changes, then f will be called, and later on, a will be called. b and c should not be called. But I'd double check it. > :component-did-update actually seems like the most clean way to do this Depends on what you mean by "clean". :) It's definitely the most clunky. And just comparing the arguments with = (IIRC) is the default behavior of :component-did-update.

Sam Ritchie18:03:48

what I meant is that it seemed like the cleanest place to update the atom with the compiled code

Sam Ritchie18:03:01

vs checking it inside the render function every single time

Sam Ritchie18:03:40

I will eventually have a working example to share but there is another bug in my way

p-himik18:03:35

Yeah, that's true about updating a ratom there. But many (including me) tend to ignore that. It'll lead to two renders and one call of the slow function, but it'll save ~10 lines (unless you write some sugar around :component-did-update).

juhoteperi18:03:39

> like calling “fmap” or “map” on a reactive value Sounds like reaction. Your example doesn't have the reactive value though.

Sam Ritchie18:03:09

@U061V0GG2 well, the “reactive value” is the argument f; the component is a function of the state, and only re-renders when the state f changes

Sam Ritchie18:03:35

so I want to say “well, actually stick a few transformations onto the end of f, but still only re-render when something is pushed into that pipeline”

juhoteperi18:03:49

If the source is RAtom:

(defn fn []
  (let [f (r/atom ...)]
        res (reaction (sci-eval @f))]
     (fn []
        @res)))
If f ratom value changes, the reaction is run, if the reaction result changes, component is re-rendered. If the value is just a component property, react/useMemo might be easiest fit.

👍 1
juhoteperi18:03:08

Source RAtom could be a another Reaction also. Or Re-frame subscription. And the eval call could be in a Re-frame subscription also.

Sam Ritchie19:03:38

that is great!!

borkdude19:03:08

I was reminded of re-frame but that's just built on reagent reactions :)

Sam Ritchie19:03:48

the only tricky part is that f is user-supplied, so…

(defn fn [f]
  (let [f' (r/atom f)]
        res (reaction (sci-eval @f'))]
     (fn [_]
        @res)))

Sam Ritchie19:03:07

that snippet as I wrote it doesn’t really work; we want to apply the reaction to the incoming parameter

Sam Ritchie19:03:15

is there some way to do that?

borkdude19:03:23

where does the user input come from? you could modify the atom on change?

p-himik19:03:18

Yeah, since it's an argument, you can't use reaction without going with that "pair" approach and resetting the ratom in the view function. And at that point, it doesn't make sense to use a reaction - you can just compute the value right away.

juhoteperi19:03:31

It is possible to workaround this by updating the value on render. There can be some benefit to this.

(defn foo [f _x]
  (let [f' (r/atom f)
        res (r/reaction (do (js/console.log "run reaction")
                            (str "x" @f')))]
     (fn [f x]
       (js/console.log "render" x)
       (reset! f' f)
       [:span "result" @res])))

;; asd and bar are r/atom or something

           [foo @asd @bar]
           [:input
            {:value @asd
             :on-change #(reset! asd (.. % -target -value))}]
           [:button
            {:on-click #(swap! bar inc)}
            "render"]
The ratom in the component is updated on each render, but the reaction is triggered only if the source value changed. So in this example, if the second value is changed, the component re-renders, and it calls reset!, but the reaction isn't triggerd as the value didn't change.

juhoteperi19:03:58

But if your component only takes the one parameter, it shouldn't be rendered anyway if the parameter didn't change. So this example is only useful if you also take other parameters, which shouldn't trigger some calculation.

Sam Ritchie19:03:48

@U061V0GG2 okay, that is a good example

juhoteperi19:03:50

If I needed to do something like this, I would probably use useMemo or find some way to avoid the workaround.