Fork me on GitHub
#fulcro
<
2021-06-23
>
Björn Ebbinghaus12:06:14

@tony.kay Is there a semantic difference between nil props and {} ? Asking, because I noticed, that (computed nil {:something 42}) => nil and computed explicitly checks for nil props. (when-not (nil? props) ...)

Björn Ebbinghaus12:06:54

Situation, where this caused a bug in my client, because there is no state for a child, as it only has a query from root:

(defsc Child [_ _ {:keys [something]}]
  {:query [[::uism/asm-id ::My-example-Router]]}
  something) ; => something is nil!

(def ui-child (comp/computed-factory Child))

(defsc Parent [_ {:keys [child]}]
  {:query [{:child (c/get-query Child)}]}
  (ui-child child {:something 42})
I thought I didn't need to include any initial-state for the child...

Jakub Holý (HolyJak)15:06:41

There is I think and if you have only a link query then you must have initial state, even if empty for it to work as expected https://book.fulcrologic.com/#_a_warning_about_ident_and_link_queries

Björn Ebbinghaus15:06:39

Thanks. I was sure, there is an explanation somewhere in the docs. 🙂

Björn Ebbinghaus15:06:48

In this case, computed not working with nil is there so that it breaks intentionally? If, so. Maybe this would be a place to add one of these "Warning: You probably messed up." log messages.

tony.kay15:06:47

if you got no props, then you should be able to detect that it is unnecessary to render a (stateful) component (because the props are nil)…computed should not change that, but it requires props to hang the computed data.

tony.kay15:06:06

so yes, computed intentionally does nothing if there is “nothing” to work on

tony.kay15:06:21

if it is a non-stateful component, then you don’t use computed, you can just pass whatever you want as props

tvaughan16:06:09

Is there a way to apply nilify-not-found globally? Currently it seems like I have to add this to the pre-merge hook of every component, correct?

Tyler Nisonoff17:06:44

In the Fulcro RAD source code: Theres an example of a pathom plugin that replaces ::p/not-found with nil — is that sufficient? com/fulcrologic/rad/pathom.clj -> (p/post-process-parser-plugin p/elide-not-found) ->

(defn elide-not-found
  "Convert all ::p/not-found values of maps to nil"
  [input]
  (elide-items #{::not-found} input))

tvaughan17:06:22

Thanks @U016TR1B18E. I'm not using RAD, but I am using Pathom with elide-not-found. Provided I understand things correctly, Fulcro then turns these nils into :com.fulcrologic.fulcro.algorithms.merge/not-found once received on the client-side. Note the use of nilify-not-found in the Countdown component at https://book.fulcrologic.com/#_pre_merge_with_server_data

Tyler Nisonoff17:06:35

ahh got it, i’ll poke around — with my RAD set-up I never see not-found in the client db, so something must be fixing your issue (assuming thats what you’re referring to)

tvaughan17:06:23

I can see in the inspector that things do indeed come back as nil. From that same link a bit further down: 1. During the normalization step of a load Fulcro will put `::merge/not-found` value on keys that were not delivered by the server, this is used to do a `sweep-merge` later, the `merge/nilify-not-found` will return the same input value (like `identity`) unless it’s a `::merge/not-found`, in which case it returns `nil`, this way the `or` works in both cases. On a re-read, now I'm not sure if what I'm seeing is expected behavior or not

Jakub Holý (HolyJak)18:06:55

What behavior are you seeing? IMHO there is never :not-found inserted into the DB. It is visible in pre merge but later gets removed / ignored, I believe.

tvaughan19:06:55

> What behavior are you seeing? The value of a property in a component is not-found. This property has a value of nil set by the backend. Pathom correctly strips this property (key and value) from the response.

Jakub Holý (HolyJak)19:06:05

So the client DB physically contains the :<some ns>/not-found key as the value of the prop, i.e. (get-in client-db [:my-entity/id :some/prop]) returns this kwd?

tvaughan19:06:35

(:some/prop props) returns not-found. I don't see not-found in the inspector

Jakub Holý (HolyJak)19:06:40

Hm, that is strange. What if you do fdn/db->tree ? Do you see the not-found there?

tvaughan20:06:10

Sorry, I don't know what I'm doing wrong, but this keeps coming up {}

tvaughan20:06:40

I got something to print, but it's just state machines, sessions, and site chrome. The components I'm dealing with don't show up

tony.kay22:06:52

The ::not-found marker is only present during the guts of merge, so that merging works “as expected” (things you didn’t ask for cannot be affected, but things you did ask for that are missing get removed). :pre-merge has some specialness, since it is sort of participating in this story. So, not sure what you’re asking exactly.

tony.kay22:06:33

The built-in behaviors of Fulcro will not propagate ::not-found to app state

tony.kay22:06:19

though you can, of course, put it there. I don’t use pre-merge much, so off the top of my head I do not remember what extra things you need to do. I’d honestly have to read the book (or source) again to remember.

tvaughan22:06:28

> So, not sure what you’re asking exactly. (defsc FooBar [this props] {:query [:some/name]} (dom/div (:some/name props))) renders as <div>:com.fulcrologic.fulcro.algorithms.merge/not-found</div> even though this prop is returned as nil by the backend (more accurately :some/name nil is removed by pathom from the response map). I would like to know how to make sure that (:some/name props) returns nil not ::merge/not-found.

tvaughan22:06:44

The only thing I've found relevant in the fulcro book is the use of nilify-not-found in the pre-merge hook. It's not that I want to use the pre-merge hook. I assume that this is either the correct way to solve this problem due to lack of any other documented approach that I've been able to find, or I'm doing something wrong.

tony.kay01:06:37

What version of Fulcro, and have you customized anything in the app? This is defeinitely not std behavior

tony.kay01:06:33

ah, you don’t have an ident on the component, which is a very non-standard thing to do with loaded data. It could be there’s an undiscovered-till-now bug with sweep merge in that code path, which almost no one (well, at least in my circle) uses.

tony.kay01:06:01

unless you’re making up a small example for this thread, and are not actually showing the real code you’re using

tony.kay01:06:17

otherwise, it could possibly be a regression…but I think there are tests covering most of that

tvaughan01:06:34

Just something I made up to help illustrate the problem. I’ll post a complete example tomorrow

tvaughan21:06:05

@tony.kay I created a gist on github, https://gist.github.com/carrete/fd98413cd3fb1a6b1ea649f5814e437f. This includes a portion of the actual component source, and two screenshots of the inspector showing two different server responses to the same query. The problem is triggered based on the server response. When the response to {:drawing/editor-session-ref [:editor-session/id :editor-session/pid]} is missing completely from the response map, things work as expected. But when the response is :drawing/editor-session-ref {:editor-session/id "..."} :editor-session/pid is rendered as ::merge/not-found, i.e. the function editor-session-link at the top of example.cljc in the gist returns "/svg-editor/::merge/not-found/". This is with fulcro v3.4.22 and pathom 2.3.1. This project is about a year and a half old. I'm not sure what you mean by "customized anything in the app." We didn't start with a template, though I took a lot of inspiration from the youtube videos (and my prior experience with om.next and reagent/re-frame). This has evolved quite a bit in the past year and a half, but other than including server-side rendering, I don't think we're doing anything out of the ordinary. Please let me know if there's anything else you'd like me to provide or do to help with this, perhaps create an issue on github if you think this could be a bug. Of course, I won't be surprised if you say I've done something wrong. This is also working for us now that we know how to work around this so please feel free to ignore this too. Thanks!

tony.kay21:06:43

you’re passing the props of one component to another, didn’t compose the query properly, and therefore the load cannot possible do the correct merge.

tony.kay21:06:23

If the SVGEditor has a query, and is joined ot the component above it, it better darn well have a join in the parent query

tony.kay21:06:05

This falls into the 90% category: most questions on this channel are incorrect query/ident/initial state. 😄

tvaughan21:06:43

Thanks @tony.kay. I’ll go back to the documentation and try again.

👍 2
Tyler Nisonoff17:06:08

I’m curious about the right composition approach here when the parent seems to be the same entity (and perhaps the same/similar query?) as the child component. I know pathom placeholders are one option, but that doesn’t work with form state.

tvaughan23:06:19

Same. I thought it was recommended that components which are simply different views of the same data should share the same ident, and therefore props.

👍 3
Tyler Nisonoff23:06:59

I don’t think thats inherently true, as two views of the same data may expect different data (via different queries) For example, PersonView may only care about :person/name and :person/age But PersonDetailed may care about :person/name and :person/description We wouldnt want to pass PersonView props to PersonDetailed, because it’d be missing :person/description , even if they share the same ident However I’ve ran into a few situations where I’ve had multiple views on the screen all showing different fields of the same ident, and haven’t figured out a great way to modularize this in a way that plays nicely with form state + loads

tony.kay00:06:17

Two components that use the same data should use the same ident, so that they co-exist in the db, but all component should query for their own props. If I have a row in a PersonList called PersonRow, and a form called PersonForm those two very definitely would have the same ident. That does change the fact that they would have their independent and reified path in the database One lives in a table, the other lives on a screen for editing. If you have some weird case where you want a child, but that child is using the same props as the parent, then you have two options: 1. The child is a stateless component or just a plain function. It does not have a query or ident…if it purely a function used by the parent to render some content. It doesn’t participate in I/O, etc. It is a pure rendering concern. 2. Join the child in. That is fine as well, BUT the child must be properly joined, have its own query/ident/initial state SUCH that the parent joins the child in. Now, in this case since the parent and the child are the same, it means the state of that in the db will just be a loop:

{:thing/id {1 {:thing/id 1 
               :thing/parent-fact initial-parent-fact
               :thing/child-fact initial-child-fact
               :thing/child [:thing/id 1]}}}

tony.kay00:06:32

and the components would be:

(defsc Child [this props]
  {:query [:thing/child-fact]
   :ident :thing/id
   :initial-state {:thing/id 1
                   :thing/child-fact initial-child-fact}}
  ...)

(defsc Parent [this props]
  {:query [:thing/parent-fact {:thing/child (prim/get-query Child)}]
   :ident :thing/id
   :initial-state {:thing/parent-fact initial-parent-fact 
                   :thing/id 1
                   :thing/child {}}
  ...})

Tyler Nisonoff00:06:48

Thanks Tony > That does change the fact that they would have their independent and reified path in the database One lives in a table, the other lives on a screen for editing. Similar to the example above, how would you handle loads in this case? I feel like I end up mixing up UI-concerns (edges in my UI database) with server concerns (pathom resolvers that can resolve those edges) So I can use my UI component composition to handle my loads. I can choose to use a separate component all-together for loads, but then now I have to remember to update this LoadComponents query any time I update any of my sub-components. Is that just a trade-off we have to make for ourselves?

tony.kay00:06:03

If you are going to load this from a server, then you have to make a resolver that understands that :thing/child is just a loopback. This is what placeholders are for in Pathom (plugin that allows you to use them without having to write a special resolver)…so whatever naming convention you use for placeholders (often > as a prefix) will make pathom work with this nesting full stack

tony.kay00:06:18

and in that case, of course, no need for initial state since it all comes from the server.

Tyler Nisonoff00:06:47

got it Only downside of pathom resolvers is i believe form-state doesn’t understand it, but perhaps thats too much of an edge-case. (For example, I have a view where the left-side panel is a form of an entity, and the rest of the screen contains many different views / graphs of that entity)

tony.kay00:06:56

and yes, if you want to split your UI up and have the localized queries then you have to add the pathom placeholders plugin to get it to work well. But notice you cannot even imagine doing that in stock GraphQL (well, you could add it to the schema, and the add resolvers, etc…but being able to arbitrarily do this kind of magic from the client with no backend coding is kind of magical IMO)

tony.kay00:06:34

Ah, form state. So, I did not design form-state with this in mind, no, but that isn’t to say that it shouldn’t be capable of dealing with it

tony.kay00:06:10

That said, in my experience you can just break it apart with case (1) (stateless children/functions) and not nest the queries.

👍 3
Tyler Nisonoff00:06:45

to get around this, I added a true edge in pathom that I use for the form, and use the original edge for the other views (split up with pathom placeholders)

Tyler Nisonoff00:06:01

But I’ve also experimented with the stateless approach like you’ve mentioned

Tyler Nisonoff00:06:16

Thanks, this was helpful to talk

tony.kay00:06:42

sure. I don’t really see how your case involves form-state? The rest of the screens has many different views?

Tyler Nisonoff00:06:16

Lets say the parent component is the whole screen The left-screen child is a form of entity A The right-screen child consists of various Views of entity A

Tyler Nisonoff00:06:27

So my query on the parent looks something like:

Tyler Nisonoff00:06:39

[{:parent/editable-A (comp/get-query FormA)}
                          {:parent/view-A [{:>/view-1 (comp/get-query ViewA1)}
                                           {:>/view-2 (comp/get-query ViewA2)}
                                           ]}]

Tyler Nisonoff00:06:14

parent/editable-A and parent/view-A point to the same entity in pathom

Tyler Nisonoff00:06:05

oh and I think form-state was involved here because the form started from the parent

Tyler Nisonoff00:06:25

otherwise it may have worked as a pathom placeholder?

tony.kay00:06:42

Oh, I see, so your question was “how to handle loads”. There are several possibilities: 1. Issue loads for both. The merge is built to handle that correctly. This, in fact, is what the merge/not-found marker is about 😄 2. Issue loads for a side that is “complete” and then use targeting to patch the loaded data into the other side. 3. Generate a component (with no UI) that has a unified query for all data needed, load that, and then use targeting/post mutations to rework the graph as needed. 4. Generate pathom edges that mimic your UI shape so you can load from the parent. I find this least desirable just because it requires a lot of UI-related stuff coded into your back-end. Placeholders are nice because they don’t require addition code that is UI-related on the back-end, just a plugin. In terms of form-state that is more just initializing it correctly one the thing is loaded. That can be pre-merge, post-mutation, etc.

👍 3