reagent

Sam Ritchie 2022-11-23T12:36:22.672919Z

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 Ritchie 2022-11-23T12:36:57.141689Z

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 Ritchie 2022-11-23T12:38:10.271239Z

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

juhoteperi 2022-11-23T12:39:32.826729Z

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

juhoteperi 2022-11-23T12:39:43.422899Z

But, there might be better designs.

juhoteperi 2022-11-23T12:39:50.067819Z

Does it have to be a ratom at all?

juhoteperi 2022-11-23T12:40:53.911099Z

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

juhoteperi 2022-11-23T12:41:15.449809Z

reaction might be useful also

Sam Ritchie 2022-11-23T12:45:59.406639Z

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

Sam Ritchie 2022-11-23T12:46:35.341609Z

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

Sam Ritchie 2022-11-23T12:52:40.578609Z

@juhoteperi 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 Ritchie 2022-11-23T12:56:20.135669Z

whoops, with-let solves my problem vs let!

Sam Ritchie 2022-11-23T13:40:43.692949Z

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)]
  ...)

juhoteperi 2022-11-23T13:41:34.606759Z

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

juhoteperi 2022-11-23T13:43:36.457539Z

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

juhoteperi 2022-11-23T13:44:23.529199Z

(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 Ritchie 2022-11-23T13:44:51.805469Z

@juhoteperi 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 Ritchie 2022-11-23T13:45:01.037759Z

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

juhoteperi 2022-11-23T13:45:04.706709Z

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 Ritchie 2022-11-23T13:45:27.310319Z

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 Ritchie 2022-11-23T13:45:39.100589Z

when @!params has not changed

Sam Ritchie 2022-11-23T13:46:09.608749Z

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

Sam Ritchie 2022-11-23T13:46:56.049059Z

the caching behavior is definitely what I want here

Sam Ritchie 2022-11-23T13:48:19.395079Z

@juhoteperi maybe a bug?

Sam Ritchie 2022-11-23T13:49:13.692109Z

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

juhoteperi 2022-11-23T13:50:02.643759Z

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

Sam Ritchie 2022-11-23T13:50:43.262939Z

I’ll try to get a repro

juhoteperi 2022-11-23T13:51:07.553969Z

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.

juhoteperi 2022-11-23T13:51:25.432359Z

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

Sam Ritchie 2022-11-23T13:52:00.513479Z

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

Sam Ritchie 2022-11-23T13:56:31.796659Z

(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 Ritchie 2022-11-23T13:56:45.870269Z

BOOM! Got a repro @juhoteperi

Sam Ritchie 2022-11-23T13:57:43.781009Z

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

Sam Ritchie 2022-11-23T13:57:46.153199Z

you’ll see lots of converting calls

Sam Ritchie 2022-11-23T13:58:32.937729Z

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

Sam Ritchie 2022-11-23T14:02:35.149769Z

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

juhoteperi 2022-11-23T14:04:24.598929Z

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

juhoteperi 2022-11-23T14:05:09.797999Z

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

Sam Ritchie 2022-11-23T14:07:17.276289Z

@juhoteperi you mean the setinterval?

juhoteperi 2022-11-23T14:07:37.994209Z

(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 Ritchie 2022-11-23T14:09:02.189359Z

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 Ritchie 2022-11-23T14:09:10.023849Z

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

juhoteperi 2022-11-23T14:09:26.406629Z

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".

Sam Ritchie 2022-11-23T14:10:28.561919Z

exactly

juhoteperi 2022-11-23T14:11:22.054899Z

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 Ritchie 2022-11-23T14:12:00.994719Z

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

Sam Ritchie 2022-11-23T14:13:12.206019Z

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 Ritchie 2022-11-23T14:13:25.317559Z

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

juhoteperi 2022-11-23T14:19:32.105569Z

(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))))

juhoteperi 2022-11-23T14:20:21.433159Z

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

juhoteperi 2022-11-23T14:21:15.830239Z

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 Ritchie 2022-11-23T14:22:24.130759Z

oh yeah, that’s a good idea