Fork me on GitHub
#reagent
<
2021-08-02
>
Pepijn de Vos08:08:58

Kinda getting started with Reagent and doing a lot of (::foo @state) which as I understand it will rerun the function every time state changes, rather than every time ::foo changes. Is there like a straightforward "do this instead" or it's complicated and you have to architect your state into smaller bits? The solution probably involves cursors or reactions... Looking at https://github.com/reagent-project/reagent/blob/master/doc/ManagingState.md I should probably just make all the interesting pieces of my state atom a cursor, or just split up the state atom in smaller bits? Is there any advantage of doing the one or the other? The cursor one probably makes it easier to do atomic updates?

Pepijn de Vos08:08:52

Can I just s/(:foo @state)/@(cursor state [:foo])/ or you need to define it at the top level?

simongray09:08:11

@pepijndevos Yes, use cursors. Or alternatively, if this is in Re-frame, use subscriptions. And don’t deref the cursor directly as part of creating it (in your regex example above you are doing both at the same time). You can create it in a let (local binding) and then you can deref inside of a form-2 component’s render function. Then you will have properly separated the creation of the cursor (should happen only once) from the reactive deref (should happen every time the cursor’s value changes).

💯 2
simongray09:08:04

e.g. something like

(defn my-component []
  (let [my-cursor (cursor state [:foo])]
    (fn []
      [:div @my-cursor])))

Pepijn de Vos09:08:49

Huh.... the extra fn is just there to make sure the cursor is only evaluated once?

simongray09:08:50

It explains what form-1, form-2, and form-3 components are.

simongray09:08:40

When you return a render function in reagent, (what I did) that’s a form-2 component. When you define a render function directly, that’s a form-1 component.

simongray09:08:47

You can also do this as a form-1 component, but in that case you will need to def your cursor somewhere inside the namespace. If this is a single-use component that is definitely also possible. Regardless, you should still learn about form-2 components 🙂

simongray09:08:16

np! Good luck.

Pepijn de Vos09:08:37

But so like uh... inside utility functions and event handlers and the like, you probably want to def the cursor I guess

simongray09:08:27

No, you should never def anything inside a function. That is an antipattern. Defs should only be top-level. Use local bindings - i.e. let - if you need to access something by name after it’s been created. If you need to access it globally, then yes, by all means, def it inside the namespace.

Pepijn de Vos09:08:21

Yea that's what I mean, def at the top level. If you let it inside an utility or event handler then it will be evaluated multiple times

simongray09:08:43

Exactly. And the state will be erased every time, which you definitely don’t want.

Pepijn de Vos09:08:24

Erased??? You mean the cursor keeps some cache I suppose?

simongray09:08:50

If you re-def something, then whatever it was before will be erased.

simongray09:08:44

You should only def something once ( unless you’re debugging at the REPL).

simongray09:08:54

(sorry, I read your comment as “if you def something…” so my reply was to that)

Pepijn de Vos09:08:49

Yea I'm basically wondering how bad @(cursor state [:foo]) is (adding a let doesn't change much in this case)

simongray09:08:53

Yeah, if you calculate something in a let inside a function and then keep calling that function, you will probably be redoing that calculation.

simongray09:08:28

I don’t understand what you’re on about now. Obviously adding a let by itself doesn’t change much if you don’t make a form-2 component, but the point of a form-2 component is that the outer function is run only once, in which case you will only create the cursor once.

simongray09:08:53

The idea is to separate cursor creation from rendering.

simongray09:08:59

So don’t deref in the let , do that in the render function that the component returns, i.e. what I did in my example. Otherwise, if your component isn’t meant to be reused at all, just def your cursor inside the namespace and deref it from a form-1 component.

simongray09:08:33

Merging cursor creation and deref’ing is indeed bad if the point of the cursor is achieving better performance, which is pretty much always is. Why bother with cursors if you’re not looking to get better performance?

lilactown17:08:33

> I don’t understand what you’re on about now. Obviously adding a let by itself doesn’t change much if you don’t make a form-2 component, but the point of a form-2 component is that the outer function is run only once, in which case you will only create the cursor once. it sounds like @pepijndevos is asking what the cost of:

(defn my-comp
  (let [my-cursor @(cursor state [:foo])]
    [:div @my-cursor]))
vs. using a form-2 component

☝️ 2
lilactown17:08:54

it doesn't sound like they're confused how reagent cursors & form-2 components work on a fundamental level

lilactown17:08:07

obviously you're going to be reconstructing the cursor object every render here. the cost of that shouldn't be too high

lilactown17:08:00

it looks like, reading the source, that when you initially deref a cursor it creates a new reaction that does (get-in ratom path) and caches it on the ratom itself

lilactown17:08:44

cached-reaction is quite a hairy function but it seems that in this case it sets a field on ratom that contains a cache of reactions that depend on it, keyed by the path given to the cursor

lilactown17:08:34

the way i'm interpreting it is that repeated calls to (cursor state [:foo]) will reuse the cached reaction instead of constructing a new one

p-himik17:08:14

There's :on-dispose - wouldn't it be called for the @(cursor ...) above?

p-himik17:08:43

Probably not, given that re-frame caches its subs in a similar way.

lilactown17:08:07

yeah since reagent detects usage of the same reaction across renders, I don't think it will dispose of the reaction

lilactown17:08:18

the old cursor object will get garbage collected, but that doesn't trigger disposal

👍 2
Pepijn de Vos18:08:29

Okay so conclusion is, it's best to use form-2+let or toplevel def, but in utility functions the overhead of creating a new cursor is reasonably small? Is there a good way to profile this kind of stuff? I can't say I've tried very hard but glancing at the Firefox profiler it was kind of opaque what was causing needless rendering.

p-himik18:08:52

I don't see it mentioned above, but to make some things simpler you can use reagent.core/with-let to make your form-1 component behave pretty much like a form-2 component.

2
p-himik18:08:21

And React has a plugin at least for Chrome that should help with that kind of profiling.

2
lilactown15:08:32

yeah, I use chrome's built in profiler in the "performance" tab of the devtools to profile my app + the React DevTools profiler when I want to narrow it down to one React component