Fork me on GitHub

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: 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)

🙌 51
fulcro 21

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.


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ý (HolyJak)17:04:31

@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)? 🙏


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.


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


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.

Jakub Holý (HolyJak)20:04:50

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


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?

Jakub Holý (HolyJak)20:04:06

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


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


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


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


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


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))
        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).

❤️ 3

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)
And this result-action is the result-handler referred in the txn/combine-sends.


@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?


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.


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.


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


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


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


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.


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.


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.


ah, I see your point. That’s true


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