Fork me on GitHub
#fulcro
<
2020-06-01
>
Björn Ebbinghaus12:06:08

This feels like a big problem.. You can not really google problems you have with fulcro, because you can find most of the solutions only in slack. And slack doesn’t even save them for long … There are only five Questions on SO with the tag “fulcro” and 19 overall in the search..

fjolne17:06:53

actually problems come up not that often and Fulcro has great documentation and its source code is waaay more straightforward than of most JS frameworks that’s more of a Clojure “problem” in general, but people are getting used to reading docs and sources and asking on Slack (and argue that that’s more productive than googling, which I agree with)

fjolne17:06:46

also REPL helps a lot in resolving such issues, which ain’t feasible in most other ecosystems

tony.kay14:06:25

@fjolne.yngling this is not a true statement

tony.kay14:06:55

your problem @mroerni is that you probably don't have any state in that node in the graph.

tony.kay14:06:42

the order of the query may make some strange difference in the case when you have not reified your graph properly, but it isn't meant to be a "supported" use-case that you have to order it to get it to work.

tony.kay14:06:46

The problem is that the query will not descend into a component that has no explicit edge in the data graph. If you only query for some "other component's state" and have none of your own, then it will not work. This is an unfortunate consequence of reified views: if it isn't reified (in the data model), then it isn't there....but technically you can still call a function to render it. I have yet to think of a great way to solve that in a general way. You can use "floating roots" if you want to disconnect two sub-trees, but each node still has to have reified data.

tony.kay14:06:26

TL;DR: Add :initial-state {} to your component, and make sure the parent composees initial state to itself.

tony.kay14:06:45

that will create the reified edge

Björn Ebbinghaus14:06:48

This could be it. The node is under a router. It has an :initial-state -

Björn Ebbinghaus14:06:16

But the issue resolved itself… It somehow it started to behave like expected.

tony.kay14:06:05

perhaps you had not reloaded after adding one of those things

tony.kay14:06:12

initial state only happens on initial mount

tony.kay14:06:35

and the dynamic queries (used by the routers) also need initialization with a reload.

fjolne15:06:26

@tony.kay hm, does that mean that the target is supposed to be a singleton? I have many targets which naturally doesn’t have initial state, and dynamic routing works fine for them except for the first render with dr/current-route on their parent, which is actually not about being the first, but about dr/current-route lagging behind in time (so that timeout would fix it)

tony.kay15:06:21

The target does not have to be a singleton, no…but such things are loaded, and you do have to be careful about execution order in that case.

tony.kay15:06:36

so, no initial state in that case, but they must not be your initial route in that case

tony.kay15:06:48

i.e. they should not be first in the list of targets on a router that is in the initial “route”

tony.kay15:06:35

current-route has to use the reified graph and queries to figure out what is “on screen”…so it is time-sensitive

fjolne15:06:36

yep, the issue is in this line, but you have fallback to the first query child which fixes it https://github.com/fulcrologic/fulcro/blob/develop/src/main/com/fulcrologic/fulcro/routing/dynamic_routing.cljc#L292

tony.kay15:06:47

In the case above, the thing to note is that nothing was in the query except an ident…a case that commonly hurts ppl because they forget to give that node state of its own

tony.kay15:06:39

ah, I see. Correct, if there is not a component on-screen, then we have to assume that the route is “going to be” the default one listed in the router

tony.kay15:06:02

It’s been a while since I wrote that code, and it may not be the ideal way of answering the query. Using the on-screen index isn’t ideal in this case, and it would probably be better to use the dynamic query and current state. There will always be, however, timing issues around changing a route and what you “see” from this function call to ask “where are we?“.

tony.kay15:06:14

That is a product of the features like “will enter” and route denial.

tony.kay15:06:23

I cannot synchronously change the state to the new route.

tony.kay15:06:35

so it will always be the case that current-route lags a call to change-route

tony.kay15:06:56

That said: dynamic router has some rough edges still, and deserves more attention. I just do not have the time. It could use: • Better rendering logic to prevent route flickering. The routers should probably auto-coordinate to prevent anything from changing until everything is stable. This is kinda hard, and might even be the wrong thing to “always do”…since routers in the path might want fine-grained control. • More helpers, like pending-route to go with current-route The RAD helpers add some useful stuff, but I’ll probably leave those in RAD.

fjolne15:06:21

well, to be honest, I’m just not sure why router should care about what’s on screen and what’s not, and why it couldn’t be just a function of its state machine. I often fallback to legacy router because of its simplicity (especially when I don’t have natural URL segments for targets), dynamic router makes it really hard to reason about in more complex scenarios

tony.kay16:06:22

I would agree that the simplicity of the union-based routers is nice. Dynamic router exists because ppl kept complaining about union being too hard (i.e. you had to write all the stuff that dynamic routers is doing). So, honestly, dynamic routers was originally meant to be more of a guide to how you’d do routing with dynamic queries and add in things like “deferred routing with the ability to abort”, which is a commonly-needed thing. I avoiding writing that for a very long time just because I didn’t want to really maintain all of that…but no one ever stepped up to write something for the community, and I eventually wanted something for myself. So, “not sure why a router should care”…well, the router doesn’t care, but the actual lifecycle of the whole thing makes it a more complicated scenario. Once you add in the idea that a route can be “in progress” (e.g. due to a slow load) and might be cancelled (i.e. a user picks a different route before the prior one is “in effect) means that you now want a state machine, and have a fair amount to reason about. That said: if you have more limited requirements you are always welcome to implement your own, open-source it or not, etc. The composition and features of dynamic-router have come in super handy for building the quite feature-rich routing system for RAD, but that doesn’t mean it couldn’t use a touch-up here or there 🙂

fjolne18:06:14

yep, I see how dynamic router features are advanced ones, which is certainly a part of the complexity, and that’s great to have it in general (so thank you 🙂 ), just a few thoughts come to mind: • as I see it, “in progress”, deferred routes, etc don’t actually require target instance per se, those are things that should be taken care of by the state machine, as you already mentioned • it seems like the most vital source of complexity comes not from those features, but from the original idea of dynamic routers: separating parent from targets, which allows to refactor targets more easily I’m genuinely amazed by this latter idea, but it proved to be more difficult for scenarios like custom route authentication, query params, etc. So I’ve been thinking, what if route-segments were separated from components, so that routing would be done in a more conventional way: to have a static route tree where nodes refer to components and have their on metadata (auth, etc), and delegate the routing part itself to bidi/reitit That’s not like I’m asking you to make it, of course 🙂 I was going to do it myself for a looong time already… hope to get down to it eventually, so what do you think?

tony.kay18:06:34

whatever you feel like doing. Dynamic router’s composition, self healing under refactoring, and no need to maintain an “extra config” are all quite important to me. I’m not in agreement that it “proved more difficult” than your ideas, but you’re welcome to implement whatever you want, of course. This is my hope: people will open-source options. I think the result in RAD I’m getting from the features of D.R. is quite impressive, personally. Not sure why you think it is, per say, a blocker for any of the features you’ve mentioned. I’m doing it on my own projects with D.R., and it really isn’t that hard. You have a sequence to manage, and that is going to be touchy no matter what you use.

tony.kay18:06:45

in fact, RAD has things like “passing extra params” from one generated to component working…the route parameters are extensible, and in RAD I’ve got full EDN encoding to the URL going for every possible parameter on each screen….sort order of tables, which row was highlighted, which page you’re on in a paginated result, what sort order, etc. And it is easy to use code to route to a particular view with any of those params.

tony.kay19:06:23

so, personally, I’m very very happy with how D.R. is working in general…but it does have a rough edge here or there…nothing that can’t be fixed, but they’re tolerable problems (to me) for now.

tony.kay19:06:42

It is tempting, however, to just drop the deferred routing feature. It sounded like a good idea, but in hindsight I think the user should just manage that concern themself. Then, there’d be no need for a SM in the router, and it could just be synchronous. That would simplify it greatly, I think, but allowing routes to at least “advise” against routing away is something I’d want to keep.

fjolne19:06:38

it certainly ain’t blocker, just more difficult to reason about for me. I have both auth and query params with D.R. now, but in my implementation it just doesn’t look quite clean to say the least... I played a bit with RAD, but it’s already massive, so I put its research till better times with fewer deadlines :) I kinda appreciate deferred routing in the sense that it allows for data fetching co-located with the component itself, and that’s what I need most of the time (not complex UISM driven logic), but not in the sense that it is deferred ahah: I’d like it switch route ASAP and show loader in place of the target

fjolne19:06:20

thank you for taking time on this discussion, think I should probably first look into RAD’s implementation, chances are I’m missing something but even in the case of going down the rabbit hole with “static” routing I‘ll certainly try to open source it... though stewardship is hard, better no library than bad library ahah

dvingo16:06:43

Is it generally supported to have a subcomponent use a root link query? I'm seeing a parent component get the data properly using: {[:all-users '_] (comp/get-query User)} but rendering a nested list component {:user/list (comp/get-query UserList)} which does the same link join receives no data

fjolne16:06:29

@danvingo it is, but subcomponent should have it’s own state, not just a link query

dvingo16:06:59

thanks. I have the state map to support the nested link. The issue I'm seeing is the first render of data works fine. I'm clearing the data after the user logs out and then after a user logs in again the data is loaded and put in the top level of the state map but the link query nested component gets no data

dvingo16:06:31

adding or removing an ident on the nested component has no effect

fjolne16:06:08

do the nested components have actual data in the state-map?

dvingo16:06:44

no, just an empty map

dvingo16:06:24

i'm running the same link query in the parent and receiving data at render time, but the child component using the same link query receives nil

fjolne16:06:45

yes, that’s because a component with link query should have proper own state http://book.fulcrologic.com/#_a_warning_about_ident_and_link_queries

dvingo16:06:13

I have an empty map as initial-state like that example

dvingo16:06:22

is that not correct?

fjolne16:06:53

depends on ident, what ident nested components have?

dvingo16:06:09

it currently doesn't have one

fjolne16:06:42

ok, so as far as I understand you use children only for rendering, they don’t have natural state of their own? in this case it seems better to just pass them data from the parent, and have them defined as common React components (not Fulcro ones): (defsc Child [this props] (dom/div ,,,))

fjolne16:06:23

it might even make sense to define them as regular functions if they’re simple enough (i.e. don’t require props optimizations)

dvingo16:06:58

That would work, and what I might end up doing - I just wanted a List component that I could drop in around the app without needing to wire up the list of data it needs

dvingo16:06:21

it works on first render, the issue shows up when i clear the db for logout

dvingo16:06:26

and then repopulate

dvingo16:06:06

I think i found a solution, - was just playing around with what is proper "empty state" for the subcomponent when the data is reset

dvingo16:06:47

i think that did it- i had to set the join property on the parent to {} to get the query working again

dvingo16:06:17

on login: set the prop to the nested prop to empty map: [:component/id :my-thing :my-list] {} and insert the top level list of idents that the nested component queries for

dvingo16:06:49

i think that makes sense because it is the parallel of what initial-state is doing

dvingo16:06:34

thank you @fjolne.yngling rereading that section made me think about what the query resolver is actually doing

dvingo16:06:00

i have a related general question - what i'd really like is run initial-state for my app again for a logout + login flow. I don't see a way to do this other than hand curating the state db to some sane initial state. is there a way to issue a "re-run initial state" mutation for the entire app?

fjolne16:06:34

@danvingo ah, I thought you were trying to link query from List items, not from the List component itself. if you’re link querying from the List itself, then you don’t really need to set up its state on every mutation: your mutation works because your List doesn’t have an ident, thus its state isn’t normalized and stored inside every component it turns out to be a child of. Something like this should work:

(defsc List [this {:link/keys [data]}]
  {:query         [[:link/data '_]]
   :ident         (fn [] [:component/id :my/list])
   :initial-state {}}
  ,,,)

(def ui-list (comp/factory List))

(defsc Parent [this {:parent/keys [list]}]
  {:query         [{:parent/list (comp/get-query List)}]
   :ident         (fn [] [:component/id :my/parent])
   :initial-state {:parent/list {}}}
  (ui-list list))

dvingo17:06:32

thank you, that setup is working now 🙂

🎉 4
dvingo17:06:17

but i'm still wondering what's the best practice around resetting a db after an auth reset?

dvingo17:06:48

having to build up the state db out of band from the components is not great, that's what get-initial-state is supposed to handle

dvingo17:06:11

but i'm not sure of another way

tony.kay18:06:26

@danvingo of course you can reset state: (reset! (::app/state-atom app) (comp/get-initial-state Root {}))

tony.kay18:06:36

then just schedule a render

tony.kay18:06:19

and doing that in a mutation is even easier, as long as you know the root (which you can get from the app…so)

tony.kay18:06:15

something like:

(defmutation logout [_]
  (action [{:keys [app state]}]
    (reset! state (comp/get-initial-state (app/app-root app) {}))))

dvingo18:06:58

oh duhh, that's it!

dvingo18:06:02

thank you

😆 4