Fork me on GitHub
#fulcro
<
2022-08-17
>
Jeeves20:08:49

Perhaps more of a data modeling question than a Fulcro question, but what's the rationale behind having a two-level ID namespace (e.g. [:person/id 1234]) as opposed to just a flat namespace (e.g. just 1234 or some UUID)? The two-level namespace seems to make things more difficult in some situations. Consider Optimus Prime. Since he has a name and age and apparently a gender, should he be [:person/id 1234]? Or should he be [:truck/id 1234] with a make and model of Peterbilt 379? Or should he be [:transformer/id 1234] with two properties, one pointing to the :person/id table and another to the :truck/id table? Optimus Prime maybe doesn't need to be declared a Person or a Truck. He's just an entity (sidestepping the philosophical issue of what an entity even is) that has some properties we often associate with the concept of a person and some properties we often associate with the concept of a vehicle. Is it designed this way because most people are storing things in a relational database with separate tables for separate entity types, and defaulting to a tuple ID (containing [table-name id-number/string]) maps well to that common usecase? E.g. if we were using a triplestore, would we just put everything in one table in Fulcro? This all reminds me of nominal types vs structural types. Fulcro's approach seems analogous to nominal types, whereas I feel like Clojure culture, with its heavy use of maps, seems to lean philosophically toward structural types. But I'm a Fulcro and Clojure newbie, so I could be completely off-base here.

tony.kay23:08:49

Early in the history of Fulcro it was Om Next. Idents were derived from the concept of lookup-refs in Datomic, I believe. No further analysis was given to it, and that was many years ago at this point. In practice I have not really had it be a problem. You have to put explicit instances of a thing under SOME key in the database, and in Datomic when you give something a unique identity attribute you’ll end up with lookup refs like [:type/id id]. So, I don’t care how many different ways you think about a given thing: it has a concrete value in a database, and therefore a concrete identity. It turns out to be quite handy to have your entities grouped by their general identity type in the normalized database. Technically Fulcro doesn’t care what you use for the second element of an ident, so if you wanted to do fancy things around that (as I have with the form state ns), then have at it, and give whatever semantic meaning you want to it. [:custom-crap [:k :x 14 {:thing 22}]] is a perfectly valid ident in Fulcro. It’s just a storage scheme that has a convenient convention.

roklenarcic08:08:28

Also there is nothing stoping you from putting every ident into same namespace. So you can make every ident look like [:db/id #uuid …]

norman21:08:34

I've tried to come up with a minimal example of a my questions here: https://gitlab.com/maximoburrito/markdown-cljs/-/snippets/2391115 This is a nested routing case. I've left out the init code, but imagine (dr/change-route! app ["foo" "taco" "bar" "supreme"]) before the app is mounted. The idea is that Foo loads some data (simulated with the timeout and foo mutation) and then a nested Bar loads some other data that is in some way dependent on what Foo loaded. (the bar mutation uses the data set by foo) I'm using deferred routing on foo and bar, making the routes ready after their respective mutations complete. My expectation is that Bar would not get it's will-enter until AFTER Foo is ready. However, this is clearly not the case. Have I made some dumb mistake? Is will-enter not a reliable place to consume route parameters? Is there an example of nested routes out there that load data?

tony.kay23:08:18

Routing is about routing, not side-effects of the routes. All of the potential targets are given will-enter at the routing call. Each can defer, but those defers have NO dependency relationship. They simply allow the router to complete the UI switch at that layer. No router has any clue about what any other router is doing in the system. If you have some need for this kind of dependency management write your own thing. You can use dr as a building block, and put some kind of routing function in front of it. You ahve access to all of the keys in the component maps, etc. It actually shouldn’t be that hard if you use the dr source as a reference. The event-based handlers of dr are there for general convenience. To be honest I wrote dynamic routing as an example due to user requests, and never expected it to be the fix-all for everyone. It has some nice features and I use it heavily in production, but it doesn’t do advanced I/O management.

norman23:08:55

Thanks. I'm at a bit of a loss how people develop useful web applications without a viable routing solution. Does anyone out there have a routing solution that actually works? I suppose I can hack together something sort of react-router-ish, but I'm not looking forward to having to write something I thought fulcro had built in....

tony.kay23:08:18

What are you talking about??? Viable? It is in use in apps with 250k LOC

tony.kay23:08:21

works perfectly

tony.kay23:08:24

doesn’t do your thing is not “viablity”

norman23:08:47

Thanks for the input, sorry to have wasted your time

tony.kay23:08:15

You didn’t waste my time, you just had a mismatch of expectations. Your frustration that it doesn’t work the same as something else you’ve used is understandable. However, I’d argue that your mental model of how Fulcro works might be your weakness, and the fact that cascading I/O built into the UI layer might not actually be the best idea.

tony.kay23:08:20

My general opinion is that I/O triggered by UI components is a (sometimes useful) crutch, but that it is a poor place to put complex behavior like this. Do you really want what you’re talking about? I/O dependencies coupled by UI composition? That’s generally a nightmare

tony.kay23:08:03

If what you want is a top-level declarative routing system, then look more at the union router. It does just UI layer switching, but would be easier to hook to side-effects.

tony.kay23:08:15

IMO, what you really want is a structured state machine around the system in question, where you can properly structure your I/O, and route when the overall system is in the state you want. Complecting all that with UI is not a great idea…thus the tool in question, Fulcro, does not default to encouraging you to do so

norman23:08:25

I may be limited in my perspective coming from a few years in plain JS react, but what I really want something that lets me nest routes and lets the intermediate routers make decisions about the data. I want /foo to make decisions about the routes under /foo and I want /foo/bar to make further decsions about routes under it. I don't want to have to extract that logic out to other places in my app not concerned with those things or mixed up with other routing decisions from other parts of the app.

tony.kay23:08:31

and you’re telling me there’s a full stack solution that does this for you elsewhere? Where the instructions in question can be full-stack async operations and the routing system manages the side-effect order, completion, error handling decisions, etc?

tony.kay23:08:55

That foo could grab data from the network, error, and the routing would stop and not go to bar?

norman23:08:33

The particular pain I'm having now is, having inherited a decent sized fulcro app, we really want to switch to having meaningful URLs that can serve as entry points, but all the routing "logic" is tangled up in mutations such that if you start at "/" and click around, everything is great. But if need to be able to route deeply into views, I can't get to them. Or at least I'm struggling to do so.

tony.kay23:08:24

So I cannot speak for the skill of the prior person responsible for your code base. I can tell you the design elements and how they are meant to be used.

norman23:08:54

What my expectation would be is that /foo would be able to choose not render it's routing target until it's ready (which is what I thought deferred routing was for) and then when it's ready, it could render the bar router and the bar router could continue it making decisions from there

tony.kay23:08:18

There are many ways to implement your desire, but your side-effect dependencies between views is a problem. It really isn’t that hard to implement what you’re asking for, which is why I was pointing you to the proper functions.

tony.kay23:08:30

though if you have true full-stack effect in will-enter, then you cannot expect a call to target-ready (which runs optimistically) in the same tx

norman23:08:16

In the sample code I posted, what I really want is for the bar mutation in Bar's will-enter to happen after foo-router is ready. will-enter happens to early. The dr/current-route doesn't work from componentWillMount because Bar mounts before Foo is ready

tony.kay23:08:38

There is an assumption in dynamic routing that all routes that are about to appear are not coupled, and their deferred functions can all be submitted (and possibly run) in parallel

tony.kay23:08:26

remember that a router can technically have siblings in dynamic routing, and the targets could even be siblings of the same type that are on-screen at the same time.

tony.kay23:08:38

reasoning about such coupling is not trivial

tony.kay23:08:48

So let’s step back. Your ultimate goal is to do HTML5 style routing, where you resume a view with all data in tact?

tony.kay00:08:16

have you considered putting the data from each layer in the URL as data (hash or query params). Then each layer could read what it needed from there

tony.kay00:08:22

Also, if all you need is local optimistic changes in the suggested “cascade”, then copy change-route-relative from the dynamic router ns and simply reverse the doseq of completing functions.

tony.kay00:08:50

That will queue them in the order you’re expecting and may be sufficient

norman00:08:52

I posted some sample code above that I thought was a reasonable reduction of what I was trying to accomplish. I'd like to be able to make a route like (from the example) "/foo/:token/bar/:token" that will render the bar view with foo being able to first load data and make any executive decisions it wants to first.

tony.kay00:08:34

ok, in that case it would be a more complex extension of the system

norman00:08:46

The data I need is in the URL already

tony.kay00:08:34

“make any executive decisions it wants” is another sticking point…and again you haven’t said what should happen on error cases?

tony.kay00:08:02

If the data you needs is in the URL already, then why not just read it? Why the coupling to UI composition?

norman00:08:25

Because I don't want each Bar to have to do Foo's work

tony.kay00:08:16

Speaking in abstractions often leads to very bad conclusions. If Foo is coupled to Bar (as you’re implying) then coupling the work in Bar isn’t making it worse

norman00:08:05

That's why I spent an hour or two this afternoon trying to make a nice example code case. :)

tony.kay00:08:18

which is why I said earlier that what you likely have in these nested views is something that behaves as a unit with lots of pieces. Something like UISM lets you centralize that mess into a single reasonable unit.

tony.kay00:08:10

Right, and I’m trying to get you to think about why that example isn’t something I find a “great idea”

tony.kay00:08:35

it looks wonderful on the surface

tony.kay00:08:50

but its beauty is skin deep

norman00:08:21

Can I get an event in Bar when Foo's is ready? I really just need to call a mutation (or some code) from a nested route after the parent route is ready.

tony.kay00:08:28

Not without embedding some knowledge of each other together somewhere. (i.e. the parent)

tony.kay00:08:27

for example, the parent’s will-enter could call it’s mutation, and that mutations action or ok-action (full stack complextion) could then submit the work that the child needs, which in turn could then invoice the target ready on the child.

tony.kay00:08:14

If you want to generalize that, then add your own custom option in the child (you can add anything you want to the options map of a component), and you can pull that with component-options.

tony.kay00:08:41

but there’s no getting around the idea that the parent is going to have to know something about the child in question.

tony.kay00:08:24

Oh, I think I have an even better idea

tony.kay00:08:58

yeah, this will work really well. So, each target’s will-enter submits a transaction that records the operations it wants to do (mutations are data, so you can have a mutation that records mutations). Then you write a wrapper routing function:

(defn change-route! [app path]
   (dr/change-route! app path)
   (comp/transact! app [(do-route-ops)]))
where do-route-ops is a mutation that looks in the custom spot in app state for the things to do.

tony.kay00:08:27

Then use optimistic? false option on the transaction submitted by do-route-ops so they run to completion in order

tony.kay00:08:48

That should do exactly what you want.

tony.kay00:08:50

:will-enter (fn [app params]
  (route-deferred ident (fn []
    (comp/transact! app [(submit-op {:txn [(real-operation {})]
                                     :route-ident ident})])))
send along the route ident, and the do-route-ops can call target-ready on each as it completes

tony.kay00:08:57

or the mutation real-operation could its own target-ready…I’m just spit-balling, but it should work out exactly like you want.

norman00:08:40

I do already have my own custom change-route with a bunch of logic for default routes (fulcro routing is somewhat inflexible with default routes, like having '/some-path' show '/some-path/dashboard') so it's definitely feasible to extend that. I've already added custom props for setting the URL in the browser and other similar routing things

tony.kay00:08:48

(defmutation submit-op [{:keys [txn]}]
  (action [env]
    (swap! (:state env) update ::routeops (fnil conj []) params)))

norman00:08:21

I'll have to look at your suggestion more deeply in the morning when my brain is fresh

tony.kay00:08:32

Yeah, the default route stuff has an annoyance or two that should be fixed.

norman00:08:24

I'd love to help make that part "better". Perhaps after I get my code working.... :)

tony.kay00:08:53

anyway, the final thing you want to submit is the equivalent of:

(comp/transact! app
  [(real-operation-parent {}) (target-ready parent-ident) (real-op-child {}) (target-ready child-ident) ...] {:optimistic? false})

tony.kay00:08:24

that should work for pure optimistic AND full-stack ops just the way you want, barring errors

tony.kay00:08:36

and will compose seamlessly

norman00:08:42

I'll report back

tony.kay12:08:03

Note that the routing system executes the deferred functions in depth first order. It does this to prevent flickering in the UI, because doing it in path order would cause you to see the top level route change, then each child route in order. But since you are queuing them you can easily reverse that to path order for the operations and depth first order for target ready.

tony.kay15:08:13

I think I see a use for this for a client, so I might just code up a standard solution. I think It’s fairly trivial.

norman15:08:43

I'm coding my version as we speak. It works in my simple case, only if I do the run-ops on the will-enter of my top-level route. Doing it right along-side the change-route is too early because the ops haven't necessarily had time to queue up.

tony.kay15:08:46

The best place to do this is in the implementation of change-route-relative! . Give me a few hours. I’m just getting going this morning.

tony.kay18:08:45

ok, I’ve got it working

tony.kay18:08:57

let me write up the docstring a bit better

tony.kay18:08:03

and update the dev guide

norman18:08:31

Awesome. My version appears to be working well, but I'm still updating views and testing. I'll switch to your version and test when it's up!

tony.kay18:08:13

I don’t know if you followed all of the guidance I gave. My version works full stack, so it should be fully general.

norman18:08:32

I think your version will be more robust and more correct, but if you are curious, this is where I ended up:

(defmutation route-op [{:keys [txn target] :as params}]
  (action [{:keys [state]}]
    (println "ADD OP" params)
    (swap! state update ::route-ops
           (fnil conj []) params)))

(defmutation run-route-ops [params]
  (action [{:keys [app state]}]
    (println "RUN-ROUTE-OPS")
    (when-let [ops (::route-ops @state)]
      (swap! state dissoc ::route-ops)
      (let [real-ops (apply concat
                            (for [{:keys [txn target]} (reverse ops)]
                              [txn
                               (dr/target-ready {:target target})]))]
        (doseq [op real-ops]
          (println "REAL op" op))
        (comp/transact! app real-ops)))))

norman18:08:30

:sidebar              :messages
   :route-segment        ["conversations" :token :asset-version]
   :will-enter           (fn [app {:keys [token asset-version]}]
                           (dr/route-deferred
                            [:pages/by-id :asset-conversation]
                            #(let [token (keyword (str token "_" asset-version))]
                               (comp/transact! app
                                               [(route-op {:txn (set-active-asset-conversation
                                                                 {:conversation-token token})
                                                          :target [:pages/by-id :asset-conversation]})]))))

norman18:08:49

and that's one example usage - maybe not the best, but...

norman18:08:08

On a side topic to the above, is there a convenient way to get to the route params that go to will-enter? In will-enter you have no component and I don't think I can write it under the components ident (assuming I even know it) from here because it will be overwritten by the initial state. I can't reliably access the params from componentDidMount or from any other place that I can see. My desire to use the params may be greatly diminished with this new routing feature, but I think I may still have places where I'd like to use them

tony.kay19:08:48

The params are computed by the routing system. You get them when will-enter is called. If you need them, you have to save them (e.g. in your transaction params).

tony.kay19:08:08

with the path-ordered operation I just added, it is then trivial for children to be able to see them

norman19:08:26

I'll give it a go

tony.kay19:08:40

Note that the children have to participate to be blocked. If they don’t use the new mechanism then they’ll route without blocking for backwards compat

tony.kay19:08:08

I’m game to consider refinements