Fork me on GitHub
#reagent
<
2019-10-20
>
p-himik15:10:59

When are the bindings within reagent.core/with-let re-evaluated? I have a component like

(defn c [x y z]
  (reagent/with-let [selected (reagent/atom nil)
                     select! #(reset! selected %)]
    [... something that uses x, y, and z]))
The issue is that when y changes, the old selected value is preserved.

p-himik15:10:50

When x changes, the bindings seem to be re-evaluated.

p-himik16:10:04

Hmm. It's likely just my misunderstanding of Rea{ct|gent}. The Reagent documentation sometimes uses a notion of "component instance". And the with-let docstring says: "when used in a component the bindings are only evaluated once". I assume it means "once per component instance". But what does this "component instance" mean exactly? Namely, when does something rendered stops being one instance and becomes a different instance? I know that it happens when :key changes. Previously, I thought that it also happens when both the arguments of the instance change and its parent is re-rendered. Apparently, it's wrong, and I'm surprised I figured that out just now. I have no idea how it hasn't affected me previously.

p-himik16:10:15

Does it mean that, whenever I have something in with-let (or in the outer let of a form-2 component) that depends on the outside value, I must make the :key of the instances of that component depend on that value?

lilactown17:10:11

@p-himik yes, or alternatively if you need to re-compute some derived data based on props, do the computation in the render fn

lilactown17:10:02

the form-2/with-let and “component instance” is all circling around when React’s “mount” lifecycle runs

lilactown17:10:29

React only mounts a component again when the parent component mounts, or when it thinks that the previous component instance needs to be invalidated (e.g. your key example)

p-himik17:10:48

Thanks! Yeah, I just read a bit on how React reconciles DOM trees. Apparently, just because of how it does that, I have never stumbled upon this issue before.

lilactown17:10:05

yeah. reagent tries to make things easier by not giving us explicit control over the lifecycle of a component, but we end up with edge cases like “I want to subscribe to something based on a prop” that become hard to do

lilactown17:10:26

one option would be to invalidate the React element by using key (I think this is really hacky)

lilactown17:10:55

also potentially bad for perf because it will cause a re-mount of every child component as well

lilactown17:10:01

option 2 would be to use a form-3 component and something like getDerivedStateFromProps or componentDidMount depending on what you’re trying to do

p-himik17:10:40

In an ideal world, IMO, there should be a macro similar to with-let that would just re-evalute its bindings whenever the values it depends on change.

lilactown17:10:02

I think that would be quite difficult to write

lilactown17:10:39

you would have to analyze the code in the with-let bindings to find any usage of props, and writing the React lifecycles would be difficult

lilactown17:10:01

it also is context dependent on what you might want to do

p-himik17:10:30

Yeah, definitely.

lilactown17:10:53

with React Hooks, it covers a lot of the edge cases here. E.g.: - Running side effects on prop change

(defnc c [{:keys [x y z]}]
  ;; run `do-side-effect` only on different values of `y`
  (react/useEffect do-side-effect #js [y]))
- Only run expensive computations on prop + state change
(defnc c [{:keys [x y z]}]
  (let [[state set-state] (react/useState 0)]
    (react/useMemo #(expensive x state) #js [x state])))

lilactown17:10:02

the only one it doesn’t cover is the one I think you want, which is to just completely reset the state of the component on some prop change

lilactown17:10:23

which I guess makes sense to use key for that

lilactown17:10:50

I see a lot of people use key for running side effects and such which could really be fixed by better code practices

mazin18:10:14

Hey there, currently looking at an older Reagent project and was wondering if anyone has more context around this change: https://github.com/reagent-project/reagent/commit/ce1486a7cd4b52b9763885f33de60daeeabbdb94 Although it was possible before this commit, I assume we shouldn't use swap!/reset! on reactions directly? I think my confusion mainly arises from the comparisons to cursors in the documentation. What's the best way to handle the use case where you want an atom to react to some value, while also being able to update it elsewhere? Rather than resetting/swapping a value on the reaction itself my understanding is that you should keep the value in a separate ratom and update it using the reaction's on-set. Is there a better way to handle this that doesn't require both a reaction and separate atom?

lilactown19:10:47

@mazin Reactions are for purely derived values. If you want to swap! / reset! a reaction, you should define an on-set function when you make the reaction which updates whatever is derived from

lilactown19:10:11

otherwise, you’re likely to get into an inconsistent state. If you were to swap! or reset! the value of a reaction directly, and then the states that were derived updated, the Reaction would update again and the new state would probably be inconsistent with the state that was swapped or reset directly

lilactown19:10:54

I’m curious what your use-case is that you want to be able to update a derived value without updating its sources

mazin19:10:57

I think the use-case is mainly wanting to initialize to some value that is not available immediately and setting up a reaction to understand when that value becomes available. Ex: an input that you want to initialize to some value but that value is not present immediately so you use the reaction as the model which the input then calls swap! or reset! on on changes.

mazin19:10:38

The derived values are unlikely to update after initialization

lilactown19:10:26

hmm by “derived values are unlikely to update after initialization” do you mean the “source atoms are unlikely to update after initialization”?

mazin19:10:11

I would only expect the values within the reaction to update once

lilactown19:10:54

I think a reaction is the wrong thing to use for this

lilactown19:10:37

so if I understand: - you have some value that isn’t immediately available (e.g. some network req needs to resolve) - you have some UI state that needs to be initialized to this value, and then be uncontrolled ?

mazin19:10:38

im not sure what you mean by uncontrolled. - yes, the value is not immediately available - the value is used to initialize an input model, whose value can then update as the user types

mazin19:10:13

one way to workaround that commit is to use a reaction that reacts to that value, then use a separate ratom for the input model. then updating that input model ratom within the initial value on reaction on-set (and throw away anything that is currently potentially stored in the input model).

mazin19:10:49

i think this is the wrong use-case for reactions though

lilactown19:10:14

uncontrolled meaning that the state of the UI after initialization is not controlled by props

lilactown19:10:07

so you want something like:

(defn my-input [{:keys [default]}]
  (let [input-state (r/atom default)]
    (fn [_]
      [:input {:default-value default :on-change #(reset! input-state (.. % -target -value)) :value @input-state}])))

(defn my-form []
  (let [data (go-fetch-data)]
    (fn []
      (if (:initial-name @data)
        [my-input {:default (:initial-name @data)}]
        [:div "Loading..."]))))

lilactown19:10:04

in the above example we route around this issue by not rendering the dependent my-input until we have the data to initialize it

lilactown19:10:56

you could do something more clever if you really want to encapsulate this loading state inside my-input, but it’s not straight forward

lilactown19:10:24

funnily this is what p-himik and I were discussing earlier ☝️

mazin19:10:57

yeah, thanks, that makes sense. is using reactions directly considered an anti-pattern?

lilactown19:10:42

using reaction is not an anti-pattern, but their usage is more advanced than your every day UI concerns IMO

juhoteperi19:10:47

Using React key to force reinitialized could work also:

[:input {:key (if (nil? default-value) 1 2) :default-value default-value}]
When default-value chenges from nil to the real value, element identity changes, old element is removed from tree and new replaces it, and because the element is new, the new default value is used.

lilactown19:10:09

@juhoteperi right, p-himik and I discuss that above

mazin19:10:22

ah interesting

lilactown19:10:52

in this case, I’m guessing the input isn’t editable until you have the initial value? so I think that rendering a separate UI for that is better than using the key trick

juhoteperi19:10:13

Yeah, depends on the use case. One could use read-only prop also etc.

mazin19:10:30

yes, likely not editable

lilactown19:10:43

but I’m sort of being prescriptive about how your UI should behave which is poor form, so really do whatever you feel like provides the best UX

mazin19:10:50

thanks for clearing that up