Fork me on GitHub
#fulcro
<
2021-01-05
>
tvaughan16:01:41

I'm trying to update the property of a component in a state-machine event handler. I'm calling transact!! and I see that the property value has been set in the db when looking at it in the inspector. However, a printf in the component shows that the property is still nil. I'm assuming I shouldn't directly update the state-map from the env passed to the handler, right? Should I use something other than transact!!?

Jakub Holý (HolyJak)16:01:48

if the data changes in the DB but the component does not reflect it then I would guess Fulcro did not know it needed to refresh/rerender the component. Or?

tvaughan16:01:54

Right. I remember the book mentions the need to add a refresh flag in some circumstances. I wonder if that applies here?

tvaughan16:01:49

The property is a “ui property” e.g. :ui/error-msg, if relevant

tony.kay17:01:50

Nil likely means a spelling or graph join data error. Otherwise it's a bug, but my money's on your data

tvaughan17:01:15

I've definitely had spelling errors in the past. I'll check again. Thanks

Jakub Holý (HolyJak)17:01:37

Let me know if you stumble upon something I should add to https://blog.jakubholy.net/2020/troubleshooting-fulcro/#_frontend_fulcro 🙂

👍 3
tony.kay18:01:25

Also, try transact! to eliminate !! as a variable.

tony.kay18:01:50

@U0522TWDA some more tips perhaps for your troubleshooting. If having an issue: • Did you override the tx processing? • Are you using a non-default rendering optimization plugin?

tony.kay18:01:06

something like synchronous tx processing, rendering plugins should be avoided by beginners. The !! variants also can cause confusion because they are an extreme form of optimization that should at least be tried in a single ! variant when observing a problem, since they are an optimization that can cause confusion about whole-app rendering.

tvaughan18:01:04

@U0CKQ19AQ Should I also delete :external-config {:fulcro {:wrap-inputs? false}} ?

Jakub Holý (HolyJak)18:01:59

Thank you for the tips! I will try to find the right place for these. What is a "rendering optimization plugin"? Would :optimized-render! com.fulcrologic.fulcro.rendering.keyframe-render2/render! fall into this category?

tony.kay18:01:56

@U0P7ZBZCK those options are much less tested and used. If you’re experiencing what looks like a bug, they would be the highest suspects. I still suspect your data, esp if you’re seeing println being called, since that indicates render happened.

tvaughan18:01:36

I misunderstood the sequence of printfs. I don't see any printfs after the initial render

tvaughan18:01:35

I added a random string to the property value. I see the property value does change and has a different random number each time. However, the component doesn't update, no printf

Jakub Holý (HolyJak)18:01:46

and if you force-rerender, does the new value show up?

tvaughan19:01:34

I deleted the wrapped inputs setting, switched to transact! and added the refresh option, no change. How else would I force a re-render?

tvaughan19:01:10

I see a react error message with wrapped inputs deleted. Putting that back now...

tvaughan19:01:16

OK. Getting closer. The component also uses form config. I see the property being updated (not a form field), but it only renders in the browser after a form field is edited (which marks it complete). Still using transact! and refresh

tvaughan19:01:26

I cleared my build cache and started over. I'm back to using wrapped inputs, and transact!! without a refresh option. The property value renders when I edit a form field ...

Jakub Holý (HolyJak)20:01:04

See the troubleshooting guide - (app/force-root-render! com.example.client/app)

👍 3
tvaughan20:01:12

Thanks for the help Jakub and Tony

tony.kay20:01:12

@U0P7ZBZCK so you had some stale weird compile issue?

tony.kay20:01:14

So, just for your general edification: • !! variants only re-render the component from which the transact came • Wrapped inputs flag is largely irrelevant. The wrapping is super cheap. I never use it myself. If you don’t use wrapped inputs, then cursor jumps can happen if you do non-sync transactions, so if you add this optimization you’re asking for a “bug” that is user visible, but that doesn’t really help anything to introduce….I definitely would not use it unless I measured a performance problem with a production build. • Otherwise, component re-render is based completely on props and state, the former of which is completely based on query.

❤️ 3
tvaughan12:01:40

> so you had some stale weird compile issue? No, I doubt this is the case. I do consistently have issues with hot-code reloading. I frequently see differences between automatic hot-code reload, shift+reload, and restarting the watcher. Because I changed a compiler option I simply wanted to make sure I didn't introduce yet another variable in the equation with the build possibly being inconsistent. I'm pretty sure I made changes to the form fields yesterday during my tests, and didn't see the component re-render. I tried a lot of different things. I simply can't say I remember doing this with complete certainty. That said, this was working. I even shared a video with our team of how this form behaved; errors were displayed immediately, and disappeared when any field was edited. I've been making changes to some seemingly unrelated components. I can't think of anything that would have caused this component to break.

tvaughan12:01:59

> I definitely would not use it unless I measured a performance problem with a production build. Thanks for the clarification. I remember when this feature was introduced. We had problems with it at the time. However, I thought I remember seeing a post here in #fulcro sometime later that said to me it was stable-ish, and also the recommended default. So we turned it back on, and I haven't noticed any problems with it since. Our production traffic is light enough that we can afford to experiment a bit, and help with testing. Given the opportunity, I think it's better to adopt new features sooner rather than later, especially if they're intended to become the recommended, default approach, rather than wait until the code base grows to such a size that switching over becomes prohibitive. I misunderstood the intent of this feature.

tvaughan12:01:03

> Otherwise, component re-render is based completely on props and state, the former of which is completely based on query. Good. This has been my understanding too. However, I don't see this behavior. The only thing that appears to be different is that this component includes form state handling (and is a state machine actor). Would I be wasting my time if I looked more closely at the form state handling?

tvaughan12:01:56

> !! variants only re-render the component from which the transact came I'm using the global app in the call to transact!!. In the state machine handler I don't have access to the component, i.e. this. Is this what you're referring to?

tony.kay14:01:07

• The !! variants and unwrapped inputs sounded like a good idea at the time, but they didn’t improve performance in any measurable way I’ve been able to detect (large forms could definitely see an improvement from !!, but probably not from the wrapped vs unwrapped) • form-state is pretty old and stable. If you’re still having a problem I would put money on the data you’ve got in your state. Is it normalized correctly? Does it all join back to root? Do your queries from root join together and follow the data graph, etc? @U0P7ZBZCK I missed the fact that you are using UISM. Sorry. A state machine event handler can use apply-action to update the state map. A UISM handler MUST not do transact! or transact!!. Sorry, I missed that you were in a state machine. If you submit a synchronous transaction from a UISM, then your transaction will end up having no effect, since it runs on the thread, updates state, but UISM is meant to be side-effect free (and is in the middle of managing state for you, so it will overwrite the sync changes)

tony.kay14:01:01

Your tools in UISM are apply-action and trigger-remote-mutation

tony.kay14:01:17

Think of UISM as a “mega-mutation”

tvaughan15:01:08

Thanks a lot @U0CKQ19AQ. I'll replace the !! variants with their ! counterparts. The use of transact!! in the state machine handler was my attempt at a workaround where I duplicated properties across two components. I've since removed this and have gone back to a link query. Everything seems to be correct per what I see in the browser and inspector, except the component with the link query doesn't update until one of its properties changes even though the component in the link query has changed. For example, Component Foo has two form fields, a link query to Component Bar, and displays some of the properties in Component Bar. When a button is clicked in Component Foo, its two form fields are passed to uism/trigger! which eventually populates Component Bar. Everything up until this point appears to be correct. However, the updated property values in Component Bar are not displayed in Component Foo until one of the two form fields in Component Foo are changed, e.g. a character is added or deleted. I went back to the documentation, and Component Bar is in the initial-state of Component Foo. I'm sorry that I didn't explain this clearly from the start.

tony.kay20:01:56

If you want to generate a small repro case I can take a look. Link queries should update just fine. Most everything in your problem description should be immaterial. Render (the default one) sends props from root. I know db->tree constructs props correctly for link queries. That is heavily tested. Components will only short-circuit rendering if their props don’t change, and putting a link query in there will cause them to change assuming it is a join that includes the subprops that are actually changing, and not just the ident. There’s really almost nothing to it. Look at the source code of the renderer: https://github.com/fulcrologic/fulcro/blob/develop/src/main/com/fulcrologic/fulcro/rendering/keyframe_render.cljc#L28

tony.kay20:01:37

it is literally 3 lines of code (lines 25, 29, and 33): look up how to call VDOM library (React in this case), Convert db to a tree of props using your query, pass that tree of props to your root.

tony.kay20:01:58

Fulcro is not doing any magic, and it really helps to understand just how simple the picture is.

tony.kay20:01:51

you could run line 27/29 in your REPL and look at the props, which will be identical to a pprint of props in your Root. Is the data changing in that, because the only other thing that Fulcro does is auto-create a shouldComponentUpdate that basically says (basically) (not= prior-props-and-state current-props-and-state)

tony.kay20:01:53

Optimized render can do some additional help based on idents: It can run the query starting at a particular component to render a subtree…but the default renderer does not bother doing that unless you ask it to try (via, for example, a synchronous render or :only-refresh option).

tony.kay20:01:31

So, given your description: 1. Using link query. If you’re doing that correctly (db->tree gives back the props you expect to see), then that isn’t the problem 2. You actually have data the graph connected (can be manually checked by visually walking db in Inspect or using db->tree) 3. You understand which rendering optimizations you are triggering, but in this case you have a subtree to update, so that should not matter.

tony.kay20:01:19

so, my advice is to try running get-query on your root, use db->tree to get the props, (or just log/analyze what you’re getting in the render bodies, starting from Root)

Jakub Holý (HolyJak)23:01:59

> the default renderer does not bother doing that But isn't the default multiple-roots-renderer , which derives (I believe) from ident-optimized-render ?

tony.kay23:01:11

no and no 😄

tony.kay23:01:30

default in RAD is MRR, but in Fulcro general it is k2

tony.kay23:01:39

nothing derives from IOR

tony.kay23:01:18

😛 Shows you what I manage to remember

tony.kay23:01:27

MRR is KR2 w/floating roots

Jakub Holý (HolyJak)23:01:04

Thank you! I somehow believed that MRR is ident-optimized w/ floating roots. Great to be corrected ❤️

tony.kay23:01:04

ident-optimized was the original Fulcro approach that was based on an Om Next optimization. When I measured the performance though, it wasn’t a win for most apps.

👍 3
tvaughan17:01:22

Thanks a lot for this very detailed write-up, @U0CKQ19AQ. I really appreciate this. However, I'm not any closer to understanding what's going wrong. When I look at the current db in the inspector, or using get-query and db->tree as above, everything looks correct. Below is a very abbreviated version of what I'm working with. This has gone through many different iterations. I understand there are different ways to structure this. Depending on the approach, either 1) when :session/error changes in the Session component the SignIn component doesn't re-render, or 2) when the submit button is clicked both :email/addr and :user/password are nil (even though these have the correct values as shown in the inspector). The latter is what happens with the example below. Given everything I've seen so far in the documentation and examples this seems to be the most common approach. I see in the inspector that :email/addr and :user/password have the expected values, however props in the SignIn component only contains :sessions/session when the submit button is clicked. I also see an error message about controlled vs uncontrolled inputs. Using :value on the two input fields does eliminate this error message, but when these input fields are edited they flicker between the edited value and the value set in :value. FYI. Does anything jump out as missing or incorrect? I

tvaughan17:01:22

(defsc Session
  [_ {:keys [session/token
             session/current-user
             session/error]}]
  {:query [:session/token
           :session/current-user
           :session/error]
   :ident (fn [] [:component/id :session])
   :initial-state {:session/token nil}}
  (when (and token (not error))
    (dom/p
     (:current-user/email-addr current-user))))

(def ui-session (factory Session))

(defn- field-ok?
  [props field]
  (and
   (-> props field count pos?)
   (form-state/checked? props field)))

(defn- submit-ok?
  [props]
  (and
   (field-ok? props :email/addr)
   (field-ok? props :user/password)))

(defsc SignIn
  [this {:keys [sessions/session
                email/addr
                user/password]
         :as props}]
  {:query [{[:sessions/session '_] (get-query Session)}
           :email/addr
           :user/password
           form-state/form-config-join]
   :ident (fn [] [:component/id :signin])
   :initial-state (fn [_]
                    {:sessions/session (get-initial-state Session)
                     :email/addr ""
                     :user/password ""})
   :route-segment ["signin"]
   :form-fields #{:email/addr :user/password}
   :pre-merge (fn [{:keys [data-tree]}]
                (form-state/add-form-config SignIn data-tree))}
  (let [submit-ok (submit-ok? props)
        error (:session/error session)]
    (dom/div
     (when error
       (dom/p error))
     (dom/div
      (dom/input {
                  :id "email-addr"
                  :name "email"
                  :type "email"
                  :autoComplete "username"
                  :required true
                  :defaultValue (or addr "")
                  :onChange (fn [evt]
                              (let [value (.. evt -target -value)]
                                (set-value! this :email/addr value)
                                (transact! this [(form-state/mark-complete!
                                                  {:field :email/addr})
                                                 (ui-mutations/set-props
                                                  {:ident [:component/id :session]
                                                   :props {:session/error nil}})])))})
      (dom/input {
                  :id "password"
                  :name "password"
                  :type "password"
                  :autoComplete "current-password"
                  :required true
                  :defaultValue (or password "")
                  :onChange (fn [evt]
                              (let [value (.. evt -target -value)]
                                (set-value! this :user/password value)
                                (transact! this [(form-state/mark-complete!
                                                  {:field :user/password})
                                                 (ui-mutations/set-props
                                                  {:ident [:component/id :session]
                                                   :props {:session/error nil}})])))})
      (dom/button {
                   :type "submit"
                   ;; :disabled (not submit-ok)
                   :onClick #(uism/trigger! this :sessions/state-machine :event/create-session!
                                            {:email/addr addr :user/password password})}
                  "Sign in")
      ))))

(def ui-signin (factory SignIn))

tvaughan17:01:42

I've tried this with 3.4.10 and 3.4.12

tony.kay18:01:59

I see you have initial state, but I am concerned there is something going on where you’re perhaps clearing the state of SignIn

tony.kay18:01:08

There’s no way for me to diagnose this without a minimal repro case that actually shows all moving parts (server side, state machine, etc). The only thing I would probably not do how you’re doing it is the initial state. The {:sessions/session (get-initial-state Session) part is questionable. That data comes from root, and should really be initialized at root. I’ve never tried to do it the way you’ve done it, and don’t know that it will initialize the root stuff properly. That bit goes in the initial state of your Root component, or gets merged into root on startup. Having a random child initialize something at root as if it were part of its own state is a real potential problem…you could end up with a local ident for that field, which could (but should not) confuse the query engine.

tony.kay18:01:37

at the very least it is mixing two paradigms: link queries jump back to root. Initial state is about component local concerns.

tvaughan18:01:01

> you read this, right? Yes. As best as I can tell I've set this up properly.

tvaughan18:01:28

> and should really be initialized at root. It is.

tvaughan18:01:05

> I’ve never tried to do it the way you’ve done it I've duplicated it in the SignIn component for no particular reason. I wasn't aware this couldn't be duplicated safely

tvaughan18:01:01

> where you’re perhaps clearing the state of SignIn This is certainly a possibility. However, this looks correct when viewed in the inspector

tvaughan18:01:44

I'll see if I can create a stand-alone, reproducible example.

tony.kay18:01:17

also: don’t use defaultValue. Use :value. Control the input. :value (or email "") is how you get around nil warnings in console (or properly initialize things)

tvaughan18:01:17

I understand that's the correct approach, but I've noticed that when I use :value the form loads, something is typed in or loaded via a password manager, and then this is erased and replaced with the original value, e.g. an empty string. This happens once. Afterwards things behave as expected

tony.kay18:01:30

And you say that the tree of props you get from (db->tree (get-query Root) state-map state-map) exactly matches your UI tree

tvaughan18:01:45

It matches what I see in the inspector which is correct

tony.kay18:01:43

There is nothing unusual about your code, so I strongly suspect you have a data error. The ui is a pure mirror of the data. The rendering implementation is dead simple. If you get the data right, the UI will follow. So, when you tell me “when :session/error changes in the Session component the SignIn component doesn’t re-render” that means either: 1) you’re not using the default renderer, and are possibly getting a targeted refresh of just the session (did IT change? i.e. you log in render of both, but yo uonly saw a render of the child?). or 2) The data in props didn’t actually change (which means the data isn’t hooked up, and the ui data tree didn’t gen right as a result).

tvaughan18:01:11

> when you tell me “when :session/error changes in the Session component the SignIn component doesn’t re-render” Sorry @U0CKQ19AQ. I haven't explained this clearly. I've tried many different approaches. These approaches seem to result in one of two outcomes, either 1) the SignIn component doesn't re-render when session/error changes, OR 2) the values for addr and password are nil when the submit button is clicked. In the example above, only the second case happens. The rending of session/error works as expected

tvaughan18:01:20

Again, sorry for not having explained this clearly

tvaughan18:01:11

The SignIn component not re-rendering happens when I use transact!! and/or set-value!!, and perhaps in other cases I'm not thinking of right now

tvaughan18:01:42

At the time the submit button is clicked, the SignIn props only contains a key for :sessions/session when printed to the console, nothing else. However, these are shown in the inspector before and after the button is clicked

tvaughan18:01:26

Attempting to also display them in the html output shows that they're always nil

tony.kay19:01:59

“Shown in Inspector” is the thing I’m not sure I believe you: Are you saying they are right in their localized table, or that all of the joins from root are properly represented. Transacting and setting them will always work, but they won;t come back if your query is broken or you did not join the graph in the db.

tony.kay19:01:49

and yes, the intentional behavior of !! is not to render anything but the component used in the transact (or none if you use app)

tony.kay19:01:10

if addr/password are nil, you don’t have aconnected data graph

tony.kay19:01:45

what do the parent queries look like? What does the parent initial state look like?

tony.kay19:01:51

it all has to join to root

tvaughan19:01:19

I'm not sure I understand. The SignIn component isn't mounted in the root component, session is though. SignIn is routable and is in the root router. Otherwise it's just a "normal" component

tvaughan19:01:29

The signin component is rendered using dynamic routing in a state machine, or a redirect when a session token does not exist (as is the case here).

tvaughan19:01:36

> the intentional behavior of `!!` is not to render anything but the component used in the transact Even when I scheduled a render explicitly, or used app, I still couldn't get the render to happen

tony.kay17:01:29

If you want to post a minimal repro case I can look. This hand waving is too much a drain on my time, sorry

tvaughan17:01:49

I understand completely. I'll see what I can do. Thanks for your help @U0CKQ19AQ

tvaughan20:01:11

diff --git a/example/src/example/ui/components.cljc b/example/src/example/ui/components.cljc
index 74f76d0..45fdd5f 100644
--- a/example/src/example/ui/components.cljc
+++ b/example/src/example/ui/components.cljc
@@ -197,14 +197,17 @@
 
 (defsc Root
   [_ {:keys [sessions/session
+             root/signin
              root/site-chrome]}]
   {:query [{:sessions/session (get-query Session)}
+           {:root/signin (get-query SignIn)}
            {:root/site-chrome (get-query SiteChrome)}]
    :initial-state (fn [_]
                     {:sessions/session (get-initial-state Session)
+                     :root/signin (get-initial-state SignIn)
                      :root/site-chrome (get-initial-state SiteChrome)})}
   (if (-> session :session/current-user :current-user/email-addr)
     (ui-site-chrome site-chrome)
-    (ui-signin {:sessions/session session})))
+    (ui-signin signin)))
 
 (def ui-root (factory Root))
@U0CKQ19AQ This appears to have done the trick. To be clear, the SignIn component is not used in a link query, the Session component is. The SignIn component is also routable, so it can either be rendered here or via the site router. We have other components that have forms and are not included in the Root component (they're routable too), and they don't have the same problem (property values are nil when the form is submitted). I guess I was breaking some fundamental principle where every factory used in a component must have an appropriate entry in that component's query? Thanks again for your help and the time you spent on this

tony.kay21:01:11

yep, simple as that: UI props tree matches query tree. No query there causes no denormalizaation

Jakub Holý (HolyJak)17:01:39

The screencast demonstrating troubleshooting a Fulcro app I promised is now live https://youtu.be/1H1FZ0CEC60 - Demo of troubleshooting a Fulcro RAD app It is not perfect but hopefully useful enough.

👏 6
njj22:01:15

I’m trying to use df/load-action as follows:

njj22:01:28

(df/load-action env :fetch-booking-quote CreateBookingForm
	{:target        [:create-booking :tab :quote]
	 :refresh       [f/form-root-key]
	 :params        {:payload       payload
	                 :detail-values detail-values}
	 :post-mutation `post-fetch-quote})

njj22:01:46

But when the data comes back it starts populating the state as:

njj22:01:50

[nil nil :COMPONENT/by-id :create-booking-form :total]

njj22:01:05

and the more times I change my input field, it starts adding more nils to the beginning

njj22:01:54

If I don’t pass the component in the df/load-action and just pass nil, it solves this issue but causes all my other fields to flash the pristine value on keystroke