Fork me on GitHub
#reagent
<
2024-02-26
>
Jérémie Pelletier14:02:55

is there a reason for reagent to enforce component state to be maps?

Jérémie Pelletier14:02:32

I have a CodeMirror instance I want to set as the state, without the indirection of wrapping it into a map, but this throws an assert error

p-himik14:02:38

What do you mean by "component state"?

p-himik14:02:43

And what is the assertion error?

Jérémie Pelletier14:02:47

rc/set-state, rc/state calls

Jérémie Pelletier14:02:09

because reagent itself hooks into the react state, so can't use that one directly

Jérémie Pelletier14:02:37

working around this by using a separate JS property, ie local but wondering if there's a better way

Jérémie Pelletier14:02:14

the whole atom+map indirection seems overkill when neither are truly needed

p-himik14:02:27

Heh, I've never even used that functionality, completely forgot about it. > reagent itself hooks into the react state, so can't use that one directly Not sure what you mean. Why not just use a plain atom, if you need for it to not be a part of the React life cycle? Or a ratom, if it must be a part of that. > the whole atom+map indirection seems overkill when neither are truly needed That's just the convention Reagent chose, and it's the only reasonable one that works when you need set-state to actually merge the state and not replace it altogether. I wouldn't call it an overkill - how much harm can a one map do? And an atom is needed in order to make the component react to state changes, since it's actually a ratom.

Jérémie Pelletier14:02:27

ah in this case its not used for reactions, but used with a create-class component with lifecycle methods; had performance issues at scale last time I naively put atoms everywhere

p-himik14:02:16

You can put a regular atom in a let right outside the call to create-class. Or react/createRef, if it's a proper ref. Or a JS object, whatever you prefer.

Jérémie Pelletier14:02:40

ah didnt consider wrapping the whole create-class around a let, thanks!

Jérémie Pelletier14:02:48

thought that was form-2 only

p-himik14:02:02

That's not for any specific kind of components, it just makes the functions close over some values that you chose. It can even be around a form-1 component, although those values will remain static after being initialized at the ns loading time.

p-himik14:02:35

(Unless you crate form-1 components as lambdas, of course).

Jérémie Pelletier14:02:11

yeah also trying to avoid long-lived lambdas, they tend to be a nuisance when live editing code 🙂

Jérémie Pelletier14:02:50

or if I do, i made a wrapper dev macro to wrap the call around another lambda, so the original function can still be redefined from cider

p-himik14:02:24

Sounds complicated and I have no clue what it all means. :D On the frontend I use the automatic hot reloading by shadow-cljs, I don't have to think about long-term anything, apart from the state of course.

Jérémie Pelletier14:02:29

ah, well imagine you have references to functions inside that state, ie a map from keybindings to command functions, just reloading code wont cause the state to automatically point to the new definitions

Jérémie Pelletier14:02:57

or registered event handlers, this is a more common case; ie mediaDevices.ondevicechange

p-himik15:02:55

I see. I very, very rarely encounter those scenarios myself. Possible factors for why: • Such a function is going to be re-set on a hot code reload (because loading a particular ns sets the value) • Not the function itself is referenced but some indirection (like e.g. event IDs in re-frame) • React does it for me > registered event handlers An event listener can be not only a function but also an object that wraps a function. E.g. I do this to listen to keyboard events globally:

(defonce -listener (let [listener #js {:handleEvent (fn [_])}
                         opts #js {:capture true}]
                     (js/addEventListener "keydown" listener opts)
                     (js/addEventListener "keyup" listener opts)
                     listener))

(defn stop-recording! []
  (set! -listener -handleEvent (fn [_])))

;; For hot reload.
(stop-recording!)
And later in the code I can (set! -listener -handleEvent (fn [evt] ...)).

p-himik15:02:20

Of course, it's an ad-hoc solution. But the problem is also kinda ad-hoc. Definitely not worth it to switch from the automatic hot reloading to any manual approach, at least for me (although on the backend I use the "reloaded" workflow with a manual trigger).

Jérémie Pelletier15:02:04

ah yeah, trying to avoid having to add manual steps to iterations, I just do (js/addEventListener "keydown" (cb some-function)) then cider-eval-last-sexp over (defn some-function [e] ...) makes it live instantly; saving the file and waiting for the whole reload flow is ~2-5 seconds on this project

Jérémie Pelletier15:02:03

where cb is that macro I made, which is identity in optimized builds, and wraps the call in development to enable this flow

p-himik15:02:23

Ah, I see. 2-5 seconds sounds a bit much though. Quite a bit.

Jérémie Pelletier15:02:41

yeah its over 1 minute before the JVM optimizations kick in haha

p-himik15:02:03

...what? How do you hot reload your frontend code?

p-himik15:02:16

Or did you mean the backend?

Jérémie Pelletier15:02:37

ah I mean the cljs compiler running over the JVM, on a fresh start the initial compile is slow

Jérémie Pelletier15:02:01

second one drops to 10 seconds, then its 2-5 secs, hotspot seems to be working great here

p-himik15:02:58

Those timings are bizarre. You are not using shadow-cljs, are you?

p-himik15:02:24

It might not be hotspot but rather some caching. Shadow-cljs has great approaches, timings are pretty much never that high.

Jérémie Pelletier15:02:42

yeah its probably the extensive macro work im doing in most cljs namespaces, and reloads only doing partial compiles of the affected dependencies

p-himik15:02:00

The initial start is around 15 seconds, all subsequent hot reloads (including the time to make the whole web page re-rendered) are less than a second.

Jérémie Pelletier15:02:09

wouldnt that depend on project size?

Jérémie Pelletier15:02:42

say 50+ cljs files, sometimes going over 100kb, and extensive macro transformations (ie clj -> wgsl+js with full data structures)

Jérémie Pelletier15:02:41

persistent maps are great, but also orders of magnitude slower than mutations when chaining transforms

p-himik15:02:49

Only the very first compilation would depend on the project size a lot, when there's no cache at all. Any subsequent compiler server launch is much faster because most of the stuff is cached. Any subsequent compilation and reloading of a changed file is even faster because the server is already running and even more things are cached. The only thing that can make it slow is if you change some ns that's a direct or transitive dependency of most other ns'es - then they all have to be reloaded. > say 50+ cljs files, sometimes going over 100kb The timings I provided above are for a project around 10 times larger. Much more than that if I include all of the dependencies that shadow-cljs also has to compile.

p-himik15:02:57

> and extensive macro transformations (ie clj -> wgsl+js with full data structures) You should be able to cache those. > persistent maps are great, but also orders of magnitude slower than mutations when chaining transforms I don't know what it has to do with anything. Maybe you meant it as an explanation of why you're using wgsl+js, but I just have zero knowledge in the area.

Jérémie Pelletier15:02:18

ah i mean most of the cljs code is driving a transpiler to generate WSGL shaders and the equivalent webgpu bindings, probably could do some caching right now it's re-evaluating the entire thing when a ns is recompiled