Fork me on GitHub
#fulcro
<
2022-09-15
>
zhuxun214:09:38

I see what is going on now. My index.html is a bit different from fulcro-template in that it loads the main.js asychronously rather than including it as a <script> tag directly

zhuxun214:09:57

This seems to break the fulcro inspect's ability to connect with the app

zhuxun214:09:35

By changing the index.html to loading the main.js directly, the inspector now works as expected

zhuxun214:09:34

My next question is, if the inspector cannot connect to the app at the initial loading, is there a way to manually make the connection once my app is asynchronously loaded?

Ernesto Garcia16:09:27

Hi. Why is it that to-many relations are only normalized when given a vector? I have just been caught by this gotcha, which I find quite obscure.

tony.kay17:09:20

Technically (and historically): because the design and EQL standard requires a response of many-things to be encoded as a vector. Lists in the notation are meant for parameters in the query itself, but nothing in the EQL standard allows for lists as part of the return syntax. It would probably be reasonable to just “be nice” and accept them when they are unambiguous.

Ernesto Garcia18:09:07

I think that is reasonable. It also puts a burden on the server side to realize a vector, doesn't it? There may be circumstances where just serializing a lazy list would be more performant.

Ernesto Garcia18:09:49

I don't see the place in the spec that disallows lists as the type of sequences in result data.

sheluchin18:09:41

I thought part of the reason was related to Pathom's batch resolvers requiring vectors for their fast index access. Is this correct? If lists are allowed as a return type I think it will result in people asking about unexpected performance problems. Perhaps just some warning messages in the log would be a better middle ground?

tony.kay18:09:56

so, I’m working on approving a PR for Fulcro right now that will probably correct this issue (is that right @U0522TWDA?). I still highly recommend using vectors. Doing things like returning lazy seqs from resolvers etc is generally a poor idea for many reasons, but it is true that you can often “get away with it”.

1
👏 1
tony.kay18:09:40

Some examples of “poor idea”: • Wrapping the resolver to measure performance may be inaccurate, making it difficult to pinpoint performance problems in production (VERY important IMO) • Lazy seqs that eval out of context may lead to problems with dynamic vars that were bound at time of resolver call, but not when the actual value is serialized (triggering the lazy eval). • Does anyone honestly return “list”? I seriously doubt that’s what you’re asking for. Who here is doing (list 1 2 3) in their resolver return? No one. What you’re really asking for is (map f xs) to work, and that is a LazySeq, NOT a list. Transit just happens to encode that as a list. See prior two items.

❤️ 2
Ernesto Garcia18:09:12

That is right, I was actually referring to lazy seqs. The points you make are valid, but it is also important that you are not causing performance issues either, which can be the case if you have to realize vectors in your resolvers, which you need to build and store as a whole in memory. We could have a Transit writer that serializes all seq(uential)s to vectors?

tony.kay21:09:31

I’ve been doing production apps for 4 years now and doing some pretty aggressive stuff, and that just has never hit my radar. In theory it’s a valid point, but in practice I really don’t buy it. You’re going to have a graph query that returns hundreds of megabytes and your user is going to sit there and wait for it?

Ernesto Garcia07:09:24

I won't reach a limit for that either in my case. It can be a concern for applications with special characteristics: massive usage, concatenated resolvers on sequences (each would realize its own vector result), or just big sequences (tables/matrices...) I'm just saying there is a performance hit that doesn't need to be there for someone that might be affected.

Ernesto Garcia18:09:43

What would be a recommended way to represent lists in application data? Is it ok that list are directly accessible through a root attribute, or does it have any drawbacks? Like:

{:forecasts [{:forecast/id ...
              :forecast/name ...}
             {:forecast/id ...
              :forecast/name ...}]}

Ernesto Garcia18:09:38

Would it be better introducing an entity representing the list here? That could have the advantage of making it extensible. Like:

{:forecasts {:forecasts/contents [{:forecast/id ...
                                   :forecast/name ...}
                                  {:forecast/id ...
                                   :forecast/name ...}]
             :forecasts/count 2}

Jakub Holý (HolyJak)21:09:47

Depends on the use case. Most often an entity has a attribute = vector of idents (eg Person having :friends pointing to other persons). https://blog.jakubholy.net/2022/trouble-with-lists-in-fulcro/ might be interesting

Ernesto Garcia08:09:15

I have seen your article, thanks for that one.

Ernesto Garcia08:09:44

I also have some doubts because of the way data-fetch/load! works. For fetching from the root, it requires a server-property argument, which is then ignored from the result data. This means that you can't load from your Root component, but from the child that makes use of that property contents. (This is the case for the https://book.fulcrologic.com/#_loading_something_into_the_db_root of the Fulcro book). This seems strange, as the goal is to infer everything from the Root component, but for loading we rely on Root internals. This is giving me some headache and with doubts on how I should actually design/adapt both the server-side data and the client-side component hierarchy.

Jakub Holý (HolyJak)09:09:14

> as the goal is to infer everything from the Root component That is an understandable and common misconception. I wrote https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/index.html#_what_to_load to clarify why we do not want to (df/load! Root). Does it help or can I do anything to make it clearer?

Ernesto Garcia11:09:01

Thank you, that will help! I'm gonna give it a closer read a bit later and let you know.

tony.kay17:09:50

Let me chime in here as well: I do not recommend using use-root to create a lot of roots in the app. I never use it personally. That and use-component are both meant to address dynamic situations in the application (e.g. a dropdown that needs data that you want to compose in as a black box). The general application structure is well-known for most UI elements, and should be structured that way for easier comprehension and general functionality. The composition of initial state and setting up the initial database is much cleaner if you use the standard original approach to UI creation. Also, I get that you might think that a given Root might be the thing you want to load. I strongly disagree with this approach. It’s fine for toy apps, but is, I think, a generally poor practice that leads to poor design, inflexibility, and other incidental complexities (even though on the surface it seems “simple”). So, here’s the idea for those reading this and wondering what “loading a Root” means. I write a component like this:

(defsc Root [this props]
  {:query [{:server/people (comp/get-query PersonList)}
           {:server/things (comp/get-query ThingList)}
           ...]
   :componentDidMount (fn [this] (load-root! Root))}
  ...)
The idea being that when this root mounts, all of the various server things get loaded.

tony.kay17:09:18

Here are the problems: 1. You’re tying I/O to UI concerns. If one of those queries is kinda slow, you’re waiting for the whole thing, when in fact you could have possibly rendered a partial result in a useful way. 2. Do you really want it all on initial load anyway? 3. Which of the things in the root query should be asked for in the load? E.g. which of the keys are meant as “root server query keys” and which are not? We use the :ui/ namespace for things to elide in subqueries, and we could adopt that for roots as well, but it is a concern. 4. How do you send parameters for each of these? Are some parallel? Do some need query parameters? So now you’re tempted to add in additional syntax to handle these cases:

(defsc Root [this props]
  {:query `[{(:server/people {:sort-order :ascending}) ~(comp/get-query PersonList)}
            {(:server/things {:parallel true} ~(comp/get-query ThingList)}
           ...]
   :componentDidMount (fn [this] (load-root! Root))}
  ...)

tony.kay17:09:28

but now you’re back to syntax quoting.

tony.kay17:09:37

Not only that, you’re also in a scenario where you want to drive this with the component query, which means you have to use dynamic queries (which Fulcro does support)…BUT dynamic queries are their own ball of extra complexity. What if you have two instances of the same kind of component? Does a dynamic query edit apply to one or both? Does it depend on the mount point? Etc etc.

tony.kay17:09:40

It’s a complete mess.

tony.kay17:09:10

So, my opinion is that loading is logic OUTSIDE of the UI. The UI composes and queries for what it wants out of the client-side database, and the UI has no business talking about the server-side interaction of populating that data (at least from the declarative standpoint), because that story is NOT declarative. It’s dynamic and has lots of logic around it. That logic belongs in the logic sections of the application, NOT the UI.

❤️ 2
tony.kay17:09:46

The convenience of being able to do a load in componentDidMount is just that: a shortcut for extremely simple cases.

tony.kay17:09:37

In real applications you should be using things like UISM or statecharts to write the complex logic of the application. In simple cases it is certainly acceptable to trigger loads due to easy interactions (a user clicks a button or changes routes). In some cases you, in fact, do want to load various different things…but issuing a few df/load! statements is neither a source of incidental complexity or even troublesome. I would argue that it is actually the minimal complexity for the necessary granularity and features. If you want a load-root! , then write one. It is trivial to do. Just walk the root query looking for every join that isn’t namespaced to :ui/ and issue a df/load! for it. I’m not against functional composition 🙂 If you have a simple case where you want a group of loads to be derived from a single root-like component it really isn’t hard to make that function. I don’t supply one because I don’t think it is a core function that should be encouraged as a go-to way of doing things. That doesn’t mean I think it has no place in the world, it just means that I don’t think that particular pattern is one the library should encourage in the common case.

tony.kay17:09:12

(defn load-all! [app Component] 
  (let [{:keys [children]} (eql/query->ast (comp/get-query Component))]
    (doseq [{:keys [key component type]} children
            :when (and component (= :join type))]
      (df/load! app key component))))
is roughly the code (completely untested, but probably pretty close to right).

tony.kay17:09:40

BTW: the server property is not ignored. It is the default root database location in which the load result is stored. But often you want to patch that into the graph at some other location than the root. Thus :target . See this old video I made that uses old names for things, but is as relevant today as the day it was recorded when it comes to the theory of Fulcro’s operation. https://youtu.be/mT4jJHf929Q

tony.kay17:09:01

I need to redo that one with present-day library names/terms

Ernesto Garcia14:09:31

Thanks guys. The separation of component queries and load!ing in Fulcro is more clear now when explained explicitly.

genekim20:09:55

Separate topic: I am trying to save each URL to the HTML5 history when I change a Fulcro RAD control. E.g., in the Inventory Report demo, when I switch categories from Tools to Misc to _, I want to be able to go back to each previous report by hitting Back or Cmd-[ (or whatever). I called (rroute/route-to! form-this current-component newparams) , which seems to add old URL (along with the report query-params) to the history. But when I hit Back button, the URL changes, but the report doesn’t update / run. I want it to not only update the URL, but also rerun the report with the params in the URL. Can anyone help me figure this out? (If I can get this working, I’ll likely add that code to the UISM :event/run.) Thank you!

genekim20:09:39

For example, if I wanted to save each category selection into the HTML5 history, and be able to hit Back and get that report, what code would I need to add in this function? https://github.com/fulcrologic/fulcro-rad-demo/blob/develop/src/shared/com/example/ui/item_forms.cljc#L51

Jakub Holý (HolyJak)21:09:52

I'd look at the RAD history integration code and what exactly it does in the history event listener. Not sure where you need to plug in the loading of the report...

Jakub Holý (HolyJak)21:09:49

Maybe it ends up triggering some uism event but not the one you need?

tony.kay21:09:40

hm….that’s interesting. Yeah, I guess that is technically a bug. If the report parameters in the URL change due to a history change, then the report should probably trigger a run, but it had not occurred to me when I wrote what is there. I’d say the fix is to expand the history integration to find the on-screen report and trigger an event, but we probably want a new event on the report that is a “conditional” run that doesn’t take effect unless it needs to. Kind of a complex scenario, actually. If you’re ok with just triggering a report run (even if it wasn’t necessary) then that is a bit easier. Would take me an hour or two to fix that up, but I don’t have the time to do so at the moment. Here’s the basic idea I’d try (which you can easily code): 1. Write your own version of the HTML5 history (copy the existing one into your source code and install that instead of the included one) 2. When a route changes, you can use the Fulcro index of on-screen components to find all on-screen reports and get their idents (see comp/get-indexes). Use the ident->components index entry. Those indexes track the components on-screen. Look for idents that have ::report/id in their key-side…e.g. [::report/id …]. 3. Trigger a reload event on those. The ident is the UISM machine ID. (uism/trigger! app report-ident :event/run {params})

tony.kay21:09:17

I guess you could contribute that back to RAD with a config option like rerun-reports-on-route-update? that you can opt into.

tony.kay21:09:48

but then history would need to know the fulcro app in order to look up indexes

genekim22:09:18

This is a fantastic explanation, @U0CKQ19AQ — although, I am laughing at “which you can easily code”. 😂😂😂 I actually learned a ton about what routing is and isn’t during this — all I can say is, holy cow, the browser is vast, complex, and complicated. Absolutely not a priority at all — I will noodle on this, and keep you posted.

🙂 1
Jakub Holý (HolyJak)15:09:11

What I am trying to do is that inside ro/controls -> :action I essentially duplicate the top (= old) history entry; at this point the set-parameter transaction is running and when it finishes it updates the new top entry to the new url so I end up with the correct history I want. Though not sure whether I can always rely on set-parameter to have its effect on the url only after the :action is processed :thinking_face: Now when I press the browser Back, the url is updated but nothing happens in the app due to dr/change-route-relative! belief that > Request to change route, but path is the current route. Ignoring change request. Well, the route is the same, it is only the route params that have changed. I guess this is what you guys were discussing? BTW I want to try to leverage history/add-route-listener! to do the refreshing of the reports so I do not need to touch existing RAD history code...

Jakub Holý (HolyJak)19:09:13

Ok, here is a demo of a report that stores its controls past state in the URL and then re-renders itself as you browse back through the history: https://github.com/fulcrologic/fulcro-rad-demo/compare/develop...holyjak:fulcro-rad-demo:demo/browse-back-throughreport-controls-history Summary: > Normally control/set-parameter does replace-route so history of past controls states is not kept. Thus we hack the pikcer's :action to store the current state (by doubling it so that only the newly added copy is overwritten by set-parameter). > > Then we add-route-listener! to catch cases when the URL changes but the route does not (as dr/change-route-relative! ignores those) and when the url also contains params relevant to any on-screen report and in this case we force the routing to happen - this refreshing the report.

tony.kay23:09:56

So Jakub, you’re saying you want to push the route every time a parameter changes, so that every back button goes through every possible combo of parameters? Yeah, this definitely seems like a job for a new version of the History implementation, otherwise you have to hack every single form control, which is a major pain to do globally otherwise.

genekim00:09:17

I love what you did, Jakub! I’m trying it out right now — for my own purposes, this would work well enough. I don’t make any claims about how useful this would be to other people, but I love this behavior of Back in RAD forms.

genekim00:09:11

@U0522TWDA I love it! Works splendidly! Thank you!!!

Jakub Holý (HolyJak)08:09:18

Correct, Tony. But the trouble is that it isn't just history, it is also controls/set-parameter -> rad-routing/update-route-params that decides to replace-route so we would need an option for it to push instead of replace.

tony.kay22:09:21

ah, yes, that is true. It’s actually kind of a can of worms. If you sit on a report and fiddle with the parameters (e.g. increment a number field using the arrow-up button) do you want to record every step? That would make using the back button maddening. This is why I default to replacing them. You can bookmark any step, but in terms of navigation it makes the most sense to replace the route. That said, you could update the history implementation of replace-route! to be more intelligent about what it does by analyzing the route and parameters it is passed. In truth a much more complex (and configurable) system would be required to get it to work well in a general case.

💯 1
🙏 1
Ernesto Garcia08:09:44

I also have some doubts because of the way data-fetch/load! works. For fetching from the root, it requires a server-property argument, which is then ignored from the result data. This means that you can't load from your Root component, but from the child that makes use of that property contents. (This is the case for the https://book.fulcrologic.com/#_loading_something_into_the_db_root of the Fulcro book). This seems strange, as the goal is to infer everything from the Root component, but for loading we rely on Root internals. This is giving me some headache and with doubts on how I should actually design/adapt both the server-side data and the client-side component hierarchy.