fulcro

Book: http://book.fulcrologic.com, Community Resources: https://fulcro-community.github.io/, RAD book at http://book.fulcrologic.com/RAD.html
tony.kay 2021-04-07T06:59:59.410200Z

I’m continuing to clean up the new features of Fulcro related to using the state management separately from React. This work also enables easy creation of various React hook-based techniques. The verdict is still out for me as to whether this is a “better idea” than explicit composition for the main parts of apps. Peppering an app with lifecycles leads to all manner of chaos. That said, there are definitely use-cases where this kind of stuff is going to be very handy: autocomplete controls that need to load suggestions, any kind of UI control that needs access to state/network but isn’t composed from the initial frame of the app, dynamically generated UI at runtime, etc. Here are some teasers of the new features: 1. The ability to generate a component (anonymous or named) from an example. In other words, you supply a tree of data, and Fulcro generates a normalizing component that comes with query/ident/initial-state. It just requires you follow the ID naming convention:

(entity->component {:list/id    1
                    :list/items [{:item/complete? false :item/id 1 :item/label "A"}
                                 {:item/id 2 :item/label "B"}
                                 {:item/id 3 :item/label "C"}]})
2. The ability to generate a component (with no initial state) from just a query (ident is derived from naming convention):
(nc [:list/id {:list/items [:item/id :item/label :item/complete?]}])
3. The ability to “spring” said components into existence at a call site using React hooks (or an explicit call to add the component and receive data updates):
(m/defmutation bump [{:counter/keys [id]}]
  (action [{:keys [state]}]
    (swap! state update-in [:counter/id id :counter/n] inc)))

;; A raw hooks component that uses a Fulcro sub-tree.
(defn SampleComponent [props]
  (let [counter-B (hooks/use-component APP (rc/entity->component {:counter/id 2 :counter/n 45}) {:keep-existing? true
                                                                                                 :initialize?    true})]
    (dom/button :.ui.primary.button {:onClick #(comp/transact! APP [(bump counter-B)])}
      (str (:counter/n counter-B)))))
4. Root-key management, which makes loading to a target easier when you don’t have anything in state already to hang the loaded data on:
(def User (fs/formc [:ui/saving? [df/marker-table '_]
                     :user/id :user/name
                     {:user/settings [:settings/id :settings/marketing?]}]))

(defn UserForm [_js-props]
  (hooks/use-lifecycle (fn [] (df/load! raw-app :current-user User {:marker        ::user})))
  (let [{:ui/keys   [saving?]
         :user/keys [id name settings] :as u} (hooks/use-root raw-app :current-user User {})
        loading? (df/loading? (get-in u [df/marker-table ::user]))]
...
5. The ability to do a completely dynamic UISM from the above. See the various composition cards in this directory: https://github.com/fulcrologic/fulcro/tree/feature/fulcro-3.5/src/workspaces/com/fulcrologic/fulcro/cards This stuff is not yet final in terms of naming and locations, but if you wanted to check it out locally just use that branch (fulcro-3.5)

7
🙌 17
tony.kay 2021-04-07T07:04:27.413800Z

I migrated the implementation of the non-react stuff to raw/components and raw/application, and then based the existing nses off of those (so no breaking changes); however, this allows you to use those nses without ending up with a React dependency (at least that’s the intention…I may have missed something, this is early). So, all of the above stuff can be used without React at all. The new raw nses include things like add-root!/remove-root! pairs that allow you to get the state management props via a callback instead of via hooks.

tony.kay 2021-04-07T07:08:02.416200Z

UISM requires you add a :componentName option to anonymous components, and dynamic routing won’t work without being composed from root in a normal fashion…so, I have not fully baked how this stuff all composes. The dynamic anonymous components have not been tested (at all?) as stand-ins in normal Fulcro applications, but they should work. Basically, there’s a lot of little fiddly bits that probably need done still.

Jakub Holý 2021-04-07T17:15:31.421400Z

@tony.kay could you be so kind and explain a little > Component (component-name c) has a constant ident (id in the ident is not nil for empty props), but it has no initial state. This could cause this component's props to appear as nil unless you have a mutation or load that connects it to the graph after application startup. ? What is meant by "connecting it to the graph" here? Is it something like (assoc-in <the ident> {}) or is it making sure that the parent component has in the DB :some-prop <the ident> ? And is this really unique to static components, does it not equally apply to dynamic components (i.e. with ident depending on props)? 🙏

tony.kay 2021-04-07T18:37:42.421500Z

You mean for an error message? A component whose ident is constant is a component that is a singleton, right? There is only one of them possible in the database, and since the ID is constant that also means that the component in question probably isn’t something you “load”, since loaded things typically have some ID at which they normalize. So: 1. The component isn’t likely to be loaded 2. The component can only be in the database once 3. You didn’t supply initial state Therefore it is highly likely that you forgot to give it initial state and compose it into the initial state of the application, meaning that it will fail to function properly because it won’t have any data connecting it to the data graph.

tony.kay 2021-04-07T18:38:27.421700Z

This is probably the #1 problem new users run into, at least from the questions I answer.

tony.kay 2021-04-07T18:39:04.421900Z

I cannot really give a good warning for components that have a dynamic ID, because those are likely loaded, and commonly do not have/need initial state.

zhuxun2 2021-04-07T20:06:03.424300Z

Seems that the env sent to global-error-action has an empty result body (appearing as {}), even when I can clearly see in my browser devtool's network tab that the http response carries a body. Is this intentional or a bug?

zhuxun2 2021-04-08T21:19:47.432Z

Ok, I found the answer. I'm writing it down for my own future reference. First, be clear that a transaction could have multiple elements (i.e., first-level children of the ast, each correspond to a defmutation), and an element can have multiple remotes, and finally, two different elements can (and likely) have the same remote. When processing the ::send-queue, Fulcro combines sends from multiple elements when they have the same remote. This means that we need to demultiplex the combined remote response. Fulcro does the demultiplexing in the txn/combine-sends function, where it says

(doseq [{::keys [ast result-handler]} to-send]
  (let [new-body (if (map? body)
                    (select-keys body (top-keys ast))
                    body)
        result   (assoc combined-result :body new-body)]
    ...
    (result-handler result)))
There you see that if the body is not a map, each element's (i.e. defmutation's) result-action will receive a whole copy of the body. However, if the body is a map, each element's result-action will receive only a subset of the whole body, containing only the keys that it queried for (the first-level children's keys).

❤️ 1
zhuxun2 2021-04-08T21:26:39.432200Z

And global-error-action is invoked at the default result-action (i.e., m/default-result-action!) where it says:

(-> env
    (update-errors-on-ui-component! ::mutation-error)
    (rewrite-tempids!)
    (integrate-mutation-return-value!)
    (trigger-global-error-action!)
    (dispatch-ok-error-actions!))
And this result-action is the result-handler referred in the txn/combine-sends.

zhuxun2 2021-04-08T21:33:58.432800Z

@tony.kay I feel even though this way of multiplexing makes sense (it gives a valid pathom query that can be directly fed into a Pathom parser), it doesn't fit Fulcro exactly. For example, you mentioned yourself that "nodes with the same ID" cannot be handled at the moment. Maybe the multiplexing/demultiplexing can be handled using a Fulcro-specific protocol?

tony.kay 2021-04-08T23:39:53.433300Z

The issue, at its core, is this: [(f) (g) (h)], where those three mutations all have different remotes. Each of them has a result section which is going to be triggered from a separate network response. If those mutations all went to the same remote, then you can do some combining. The general problem is much harder than the common one.

tony.kay 2021-04-08T23:40:43.433500Z

So it isn’t multiplexing…each mutation has a singular existence, and can even talk to multiple remotes. Each network request needs a separate tracking node.

tony.kay 2021-04-08T23:41:18.433700Z

The combining logic that is currently present can only recombine nodes from a single transaction that all share the same desired remote.

tony.kay 2021-04-08T23:43:39.433900Z

In terms of the code you’re referncing, it is distributing result to mutations. The map in question will be keyed by dispatch symbol {'f result-map 'g result-map …}, and the top-keys of the AST of [(f)] will be 'f

tony.kay 2021-04-08T23:44:43.434100Z

so, it is possible there is some kind of bug in the transaction logic, but I kind of doubt it…you may have changed the shape of the result from the server in an incorrect way due to it being an error

tony.kay 2021-04-08T23:45:22.434300Z

Returning an error from the server that should be targeted at a mutation requires that you return a map keyed by the mutation name, which is the exact shape normal mutations return.

tony.kay 2021-04-08T23:46:32.434500Z

i.e. The result of the mutation is nothing if it failed…you should not expect its result to be anything at all, which is why you get the entire env, which I think has other keys on it around the result so you can see the raw result.

zhuxun2 2021-04-09T01:36:04.434700Z

No, I'm not saying that there's a bug. I am saying that now I understand why I get {} -- because what's in it doesn't have a key of 'f. However, I do think that global-error-action should be sent the raw http response rather than a subset one, especially when the result is in a key called body.

tony.kay 2021-04-09T01:41:29.434900Z

ah, I see your point. That’s true

zhuxun2 2021-04-07T20:22:20.425800Z

hmm... seems like when the server returns a string, the result body in env will be preserved, but not when the server returns a map

Jakub Holý 2021-04-07T20:24:50.426Z

Thank you. Yes, I am trying to clarify the error message. Thank you for the explanation!

Jakub Holý 2021-04-07T20:26:06.426200Z

I suspect the body only includes the data you have queried for, even if the server sent more. Could that be your case?

zhuxun2 2021-04-07T20:33:46.426400Z

I could be, but I can't find where fulcro does that in its source code

zhuxun2 2021-04-07T20:34:29.426600Z

perhaps it's somewhere in the tx_processing namespace. I'm still looking

tony.kay 2021-04-07T23:11:12.427Z

@zhuxun2 mutations ns, default mutation result action processes returning, and there is a custom load mutation in data-fetch.

tony.kay 2021-04-07T23:11:43.427200Z

don’t remember what global-error-action gets…