Fork me on GitHub
#reagent
<
2022-11-23
>
Sam Ritchie12:11:22

Hey all! I’ve got a question about how to manage state in a simulation I’m running. I’ve got some code running a fast simulation with a few parameters that I can change; in the hot loop, the simulation is reading the parameters from a ratom. I’ve also got a few sliders that update the value of the ratom when they move. What I want to do is write the simulation component in such a way that it can • take the ratom as input • dereference the ratom in the hot loop to get its values • NOT ACTUALLY RE-RENDER when the ratom value changes, as this triggers a recompile of the function called in the hot loop etc

Sam Ritchie12:11:57

I don’t want to recalculate updater when !params changes… I am fine if the parameters below re-render but the expensive thing is recalculating updater.

(defn PhaseVectors [!params items]
  (let [dt      3e-2
        ;; 
        updater (Lagrangian-collector state-deriv* [0 0 0]
                                      {:compile? true
                                       :parameters !params})]
    [:<>
     [box/Area
      {:width 16
       :height 16
       :channels 2
       :items items
       :centeredX true
       :centeredY true
       :live true
       :expr
       (fn [emit q p _ _ t]
         (updater #js [0 (normalize (+ q t)) p] (* dt (+ 0.01 (dec items))) dt emit))}]
     [box/Vector
      {:color 0x3090ff
       :size 5
       :end true}]]))

Sam Ritchie12:11:10

I mean, when the contents of the !params ratom change. I guess it DOES have to recalculate if a different ratom is supplied.

juhoteperi12:11:32

You should be able wrap a dereference in (binding [reagent.ratom/*ratom-context* nil] @foo) to disable deref registering listener to the atom.

juhoteperi12:11:43

But, there might be better designs.

juhoteperi12:11:50

Does it have to be a ratom at all?

juhoteperi12:11:53

Or hmm, you want to trigger something when the ratom value changes, but not just the render?

juhoteperi12:11:15

reaction might be useful also

Sam Ritchie12:11:59

@U061V0GG2 I have a few sliders that I want to re-render, since they are controlled by the value of !params;

Sam Ritchie12:11:35

that’s the only reason I want it to be a ratom

Sam Ritchie12:11:40

@U061V0GG2 actually something weirder is going on since I am getting re-renders even when the args to my component aren’t changing. digging more

Sam Ritchie12:11:20

whoops, with-let solves my problem vs let!

Sam Ritchie13:11:43

Q: in this block, is the expectation that !params will only evaluate clj->js when !clj-params changes?

(r/with-let [!clj-params (r/atom [9.8 1 1])
             !params     (r/track clj->js @!clj-params)]
  ...)

juhoteperi13:11:34

I'd write this as (reaction (clj>-js @!clj-params)) Easier to understand how reaction works than track

juhoteperi13:11:36

I don't remember what track does it it given parametrs? That might not work

juhoteperi13:11:23

(defn get-params [a] (clj->js @a))

(r/track get-params !clj-params)
Might work differently, as the deref is now inside the fn call

Sam Ritchie13:11:51

@U061V0GG2 what is happening is that if I do a reaction or track, and then I display a component like

[:pre (pr-str @!params)]
at the top level, then I can see in my console that the reaction is only called with @!params changes

Sam Ritchie13:11:01

(reaction
 (prn "converting")
 (clj->js @!clj-params))

juhoteperi13:11:04

When dereffing the atom on with-let on r/track call, it is maybe dereferenced only once on the with-let form and the track doesn't do anything

Sam Ritchie13:11:27

but if I DON’T do that, and I pass my reaction in to the hot loop where it’s derefed, then I get thousands of converting outputs to the console

Sam Ritchie13:11:39

when @!params has not changed

Sam Ritchie13:11:09

@U061V0GG2 oh yeah that makes sense if track is not a macro

Sam Ritchie13:11:56

the caching behavior is definitely what I want here

Sam Ritchie13:11:13

yeah, there is definitely something going on where deref calls to a reaction are slow until I do something like that :pre block that forces it to cache somehow

juhoteperi13:11:02

There isn't enough examples for me to say what is the problem

Sam Ritchie13:11:43

I’ll try to get a repro

juhoteperi13:11:07

You need to be careful to construct reaction in with-let or form-2 component, each reaction form creates a new reaction object. Same as creating new r/atom.

juhoteperi13:11:25

Compared to r/track which does some strange caching so that you can call it directly in the render code.

Sam Ritchie13:11:00

@U061V0GG2 yup I was careful, there’s no change between these two cases

Sam Ritchie13:11:31

(defn Repro []
  (r/with-let [!clj-params (r/atom [9.8 1 1])
               !params     (reaction
                            (prn "converting")
                            (clj->js @!clj-params))]
    (js/setInterval (fn [] @!params) 200)
    (fn []
      [:<>
       [:pre (pr-str @!clj-params)]
       [:pre (pr-str @!params)]
       [:input
        {:type :range
         :min 1
         :max 10
         :step 1
         :value (nth @!clj-params 0)
         :on-change
         (fn [target]
           (let [v (.. target -target -value)
                 v (js/parseFloat v)]
             @!params
             (swap! !clj-params assoc 0 v)))}]])))

Sam Ritchie13:11:43

If you comment out [:pre (pr-str @!params)] and reload,

Sam Ritchie13:11:46

you’ll see lots of converting calls

Sam Ritchie13:11:32

and another converting whenever you move the slider. If you uncomment, then you’ll ONLY see converting when you move the slider

Sam Ritchie14:11:35

@U061V0GG2 maybe I am missing something here though about where calls are allowed to occur

juhoteperi14:11:24

with-let and fn inside it doesn't make sense

juhoteperi14:11:09

Not sure what it does \o/ with-let generates probably something similar to form-2 component internally

Sam Ritchie14:11:17

@U061V0GG2 you mean the setinterval?

juhoteperi14:11:37

(defn Repro []
  (r/with-let [!clj-params (r/atom [9.8 1 1])
               !params     (r/reaction
                             (prn "converting")
                             (clj->js @!clj-params))
               _interval (js/setInterval (fn [] @!params) 200)]
    [:<>

Sam Ritchie14:11:02

the setinterval is just there to trigger a deref in the same way that it’s happening in my animation loop in my bigger example

Sam Ritchie14:11:10

but I would guess that your repro there has the same “bug”?

juhoteperi14:11:26

I can see the problem, but not sure if it is a bug. If the pr-str deref to the reaction value is removed, the reaction is only dereferenced inside event handlers, not in render loop or other "reactive context".

juhoteperi14:11:22

So if the ratom is updated from input, something should be updated, if the render loop updates it, there is no need to rerender?

Sam Ritchie14:11:00

what I was hoping is that the first dereference from ANYWHERE would cache the value if !clj-params does not change

Sam Ritchie14:11:12

and I am doing it this way because 1. my current code wants a js array in the animation loop, not a clj vector 2. if I use (r/atom #js […]) and try to update it from my sliders with aset, then reagent doesn’t trigger a re-render, since my mutation means there’s no equality change

Sam Ritchie14:11:25

I can also fix this by changing my animation code to take a clojure vector here

juhoteperi14:11:32

(defn Repro []
  (r/with-let [!clj-params (r/atom [9.8 1 1])
               !params     (ratom/make-reaction
                             (fn []
                               (prn "converting")
                               (clj->js @!clj-params))
                             :auto-run true)
               interval (js/setInterval (fn [] @!params) 200)]
    [:<>
     [:pre (pr-str @!clj-params)]
     ; [:pre (pr-str @!params)]
     [:input
      {:type :range
       :min 1
       :max 10
       :step 1
       :value (nth @!clj-params 0)
       :on-change
       (fn [target]
         (let [v (.. target -target -value)
               v (js/parseFloat v)]
           @!params
           (swap! !clj-params assoc 0 v)))}]]
    (finally
      (js/clearInterval interval))))

juhoteperi14:11:21

If reaction is derefferenced in non-reactive context and it doesn't use :auto-run mode, the value is not cached or something...

juhoteperi14:11:15

I might also suggest looking into React hooks, useEffect can be easier to control which changes trigger the fn call: https://reactjs.org/docs/hooks-effect.html

Sam Ritchie14:11:24

oh yeah, that’s a good idea