Fork me on GitHub
#fulcro
<
2021-09-18
>
Jakub Holý (HolyJak)20:09:41

Hello everybody! For over a year I have struggled with understanding how dynamic routers work with respect to the query. That the query [:team/id {:team/players [:player/id ...]}] works is clear. But how is it possible that also [:team/id {:whatever/my-router (get-query MyRouter)}] works and manages to retrieve data for the the routed-to player? Finally today, after much thought, I had an epiphany. I try to explain how this works in Fulcro Explained: When UI Components and Data Entities Diverge, part https://blog.jakubholy.net/2020/fulcro-divergent-ui-data/#_inserting_a_stateful_ui_component_between_a_parent_child_entities. Feedback and ideas for clarification much appreciated!

tony.kay20:09:56

I'm not sure I'd use dynamic routers for this example, as they do so many different things that the core point is kinda lost in the weeds (for me at least, and I wrote it 😄 ). Dynamic routers do switch among idents (and therefore act as you explain...as simply a new node in the graph of data), but more importantly they swap their query for current-route to that of the (new) child, which is their more important feature (an important performance optimization). If I were implementing the widget you describe I would probably not reach for a dynamic router, because the query does not need to change, only the ident. A DR is kinda overkill, and in your example I think it obfuscates the real simplicity of Fulcro's solution. If you want to page among instances of the same kind of thing, the mutation is a trivial swap of idents:

(defmutation goto-player [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state assoc-in [:component/id :PlayerGallery :current-player] [:player/id id])))

tony.kay20:09:03

The reason I would likely use a real PlayerGallery for this is I probably want to keep the list I can go through on the gallery itself:

(defsc PlayerGallery [this {:keys [current-player]}]
  {:query [{:all-players (comp/get-query Player)}
           {:current-player (comp/get-query Player)}]
   :ident (fn [_ _] [:component/id :PlayerGallery])}
  (dom/div {:onClick (fn [] (comp/transact! this [(next-player)]))}
    (ui-player current-player)))
so that I can make clicks go to the next player:
(defmutation next-player [{:keys [id]}]
  (action [{:keys [state]}]
    (let [{:keys [all-players current-player]} (get-in @state [:component/id :PlayerGallery])
          next-player (or ; wrap around
                        (second (drop-while #(not= % current-player) player-idents))
                        (first player-idents))
    (swap! state assoc-in [:component/id :PlayerGallery :current-player] next-player)))

Jakub Holý (HolyJak)21:09:22

Thank, that is a great point 😅 Do you have a tip what would be a suitable yet simple example where DR makes sense? Something like in the Book, having mutliple targets with completely different queries, such as Settings and Player? Or Possesions that include Car, House, and SpaceCraft (with their different specs) and I only want to show one possession at a time? And regarding setting the current route query - why is that so important? I believe you it is but fail to see why 😅 I guess it is an optimization b/c Fulcro does not need to construct the props tree from the client DB for the targets that are not "current" and thus saves some processing time. Is that it?

tony.kay22:09:11

correct. If you write a Fulcro app with 100 screens and manually just render whichever you want at a given time, then every render frame will require Fulcro to query ALL the data for all 100 screens. Keypress events would be abysmal (and in fact, in very early versions of Untangled this is exactly what led to the invention of the union router). The query is composed from root, so without something (union or dynamic query) to pare that down to the "feature of interest" it can add a lot of overhead. That said, I've since optimized db->tree considerably, so it is tractable to not use any kind of query factoring for performance on relatively small apps...but in general you want to be using either dynamic queries or union queries to limit the query that is evaluated on each frame. Fortunately, I already wrote a version using both...so you don't have to 🙂 On DR example: Not sure what you're asking for. Almost every useful example will have children with different queries.

👍 2
Jakub Holý (HolyJak)08:09:21

Thank you for the explanation! I understand how this limiting of the query is crucial for Fulcro and good performance but I would argue that the main reason developers adopt routing is to enable users "to move between different parts of an application when a user enters a URL or clicks an element" (and likely also to be able to load data for a subtree on demand) - query optimisation and the possibility of code splitting are side-effects. Isn't this the core feature of routing from the user's/developer's perspective?

tony.kay15:09:59

That is correct, I was just talking out the use of DR for your example really. If you were demand-loading each pane, and didn't need the router itself to understand its role (which is what makes it a poor fit in this case), then it would be a fine use. In your example I think you want the thing that is doing the "routing" to be a bit more task-aware itself.

👍 2
Jakub Holý (HolyJak)08:09:21

Thank you for the explanation! I understand how this limiting of the query is crucial for Fulcro and good performance but I would argue that the main reason developers adopt routing is to enable users "to move between different parts of an application when a user enters a URL or clicks an element" (and likely also to be able to load data for a subtree on demand) - query optimisation and the possibility of code splitting are side-effects. Isn't this the core feature of routing from the user's/developer's perspective?