This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-03-30
Channels
- # announcements (8)
- # babashka (102)
- # beginners (312)
- # calva (9)
- # clj-kondo (9)
- # cljfx (7)
- # clojure (128)
- # clojure-europe (52)
- # clojure-nl (2)
- # clojure-norway (2)
- # clojure-spec (5)
- # clojure-uk (4)
- # clojurescript (13)
- # conjure (5)
- # cursive (5)
- # datalog (18)
- # datomic (8)
- # emacs (1)
- # events (3)
- # fulcro (16)
- # graphql (2)
- # gratitude (1)
- # helix (16)
- # inf-clojure (17)
- # introduce-yourself (9)
- # java (11)
- # lambdaisland (3)
- # leiningen (3)
- # lsp (8)
- # malli (3)
- # membrane (7)
- # missionary (26)
- # nextjournal (1)
- # off-topic (19)
- # pathom (3)
- # polylith (13)
- # portal (16)
- # reagent (39)
- # reitit (2)
- # releases (23)
- # remote-jobs (1)
- # shadow-cljs (40)
- # specter (3)
- # sql (12)
- # tools-deps (8)
- # tree-sitter (1)
- # vim (3)
- # web-security (6)
- # xtdb (16)
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.
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]]]
ah! I didn’t realize the clj->js is already handled!
You can also use [:> MB/Mathbox ...]
as a shortcut to adapt-react-class inside hiccup forms
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
(into {} (map (fn [[k v]]
[(symbol k)
(r/adapt-react-class v)]))
(.entries js/Object MB))
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)])))
,,,)))
this must be a common pattern, like calling “fmap” or “map” on a reactive value
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
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.
It becomes more of a problem when your component receives more than one argument and those arguments can change independently.
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?
or, alternatively, if f
returns value [:div [a arg1] [b arg2] [c arg3]]
then only the specific components whose arguments change should re-render?
@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
but ALSO memoize is a good idea
@U2FRKM4TW :component-did-update actually seems like the most clean way to do this
> 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
.
what I meant is that it seemed like the cleanest place to update the atom with the compiled code
vs checking it inside the render function every single time
I will eventually have a working example to share but there is another bug in my way
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
).
> like calling “fmap” or “map” on a reactive value
Sounds like reaction
. Your example doesn't have the reactive value though.
@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
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”
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.Source RAtom could be a another Reaction also. Or Re-frame subscription. And the eval call could be in a Re-frame subscription also.
that is great!!
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)))
that snippet as I wrote it doesn’t really work; we want to apply the reaction to the incoming parameter
is there some way to do that?
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.
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.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.
@U061V0GG2 okay, that is a good example
thanks all!
If I needed to do something like this, I would probably use useMemo
or find some way to avoid the workaround.