Hello, I am learning fulcro with pathom3 and writing an app. I am refactoring a big component to small sub-components and want to achiecve this by grouping the props it seens this can be achieve by sth like using EQL placeholder (https://pathom3.wsscode.com/docs/placeholders), but seems that fulcro internal implement its own EQL engine for the UI, what is the typical approach doing that in Fulcro? I write some code snippets to demonstrate my question as follows, hope it's clear and i'd appreciate any guidance!
;; pathom3 can use 'placeholder' to provide specific map structure data like this:
(p.eql/process
{} ;; empty env
{:foo/a 1
:foo/b 2
:foo/c 3}
[{:>/e1 [:foo/a :foo/b]} ; mimic component query
{:>/e2 [:foo/b :foo/c]}
{:>/e3 [:foo/a :foo/c]}])
;; => #:>{:e1 #:foo{:a 1, :b 2}, :e2 #:foo{:b 2, :c 3}, :e3 #:foo{:a 1, :c 3}}
;; Can fulcro achieve similar effect like the usage of placeholder in pathom3,
;; i wanna separate a big component to smaller sub-components as follow:
;; A data-tree {:foo/a 1, :foo/b 2, :foo/c 3} passed to component Foo
;; that have sub-components E1, E2, E3 and their queries have overlaps,
;; i want to pass the data that E1, E2, E3 want from Foo
(declare E1 ui-e1
E2 ui-e2
E3 ui-e3)
(defsc Foo [this {:>/keys [e1 e2 e3]}]
{:query [{:>/e1 [:foo/a :foo/b]} ; not using (comp/get-query E1) for clarity
{:>/e2 [:foo/b :foo/c]}
{:>/e3 [:foo/a :foo/c]}]}
[(ui-e1 e1)
(ui-e2 e2)
(ui-e3 e3)])
;; sub-components E1, E2, E3, ...
(defsc E1 [this {:foo/keys [a b]}]
{:query [:foo/a :foo/b]}
...)
(def ui-e1 ...)Thanks for your detailed explanations, Tony. It take me a bit time to understand the magic, it's the power of ident and normalization. (at the beginning, the concept 'they are the same entity' is confusing, it is easy to understand sth. like 'the pepole example' is the same entity, but in practice, i write a lot ident like [:component/id :component-name] ...)
It made me realized that I migth not well understand a lot of concepts: I was relying heavily on mutations to connect attributes and manipulate the db via the 'ok-action' and hand placing the return data-tree via sth. like (swap! state assoc-in 'ident') to feed data to components. MUTATION should be used for modifying data rather than fetching it. May be i should refactor the logic using pco/defresolver and using df/load! to fetch data ...
Placeholders as you say are implemented by pathom. Fulcro doesn’t know about them and doesn’t care. If you run a legal EQL against pathom and it fills it in, great. It just works. What you’re missing is idents. Every one of these “fanned out” components is really the same entity, so they will all have the same ident.
e.g. :foo/id
that will cause the merge system to merge all of the disparate elements into the same local entity, but it will also create the faux edges that pathom returned so that the resulting entity has “pointers to itself” in Fulcro’s db. Again, it all “just works”. Fulcro’s query enging does not understand placeholders, but it will normalize them into the db when run from a remote EQL.
and you MUST make child components for these children. You almost never hand-write a subquery
(defsc FooAspect [this props]
{:query [:foo/id :foo/x]
:ident :foo/id}
...)
(defsc Foo [this props]
{:query [:foo/id
{:>/made-up-edge (comp/get-query FooAspect)}]
:ident :foo/id}
...)
If I run a load on (df/load this [:foo/id 1] Foo) then the server will return a tree like:
{:foo/id 1
:>/made-up-edge {:foo/id 1
:foo/x 42}}
which Fulcro will normalize as:
{:foo/id
{1 {:foo/id 1
:foo/x 42
:>/made-up-edge [:foo/id 1]}}}
See? Even through the query is a tree, the proper use of idents and placeholders gets you the real flat entty in the db (which is what you want) and just creates the necessary (normalized) edges that point back to the entity so that turning it BACK into a tree from the UI query will work.
Fulcro’s EQL support is VERY literal. The engine is literally read the db and follow the reified edges. We need to to be blazingly fast for render speed, so there is NO pathom-like engine IN Fulcro. The edges of the EQL graph are always reified explicitly in the db, which the normalization system will do properly on loads and such if YOU write the components to have the proper query and ident.
Pathom is the glue between your persisted model and such UI machinations. The UI has “shape desires” that don’t match the real shape, so you leverage it to “fix the difference” at these (already) costly code points (e.g. loads/mutations, which do networking and I/O).
If you need to “patch” something locally, then you have two choices, write logic in the actual UI against the data (e,g. sort or something), or write a mutation that you call which reifies your reprocessing of the data into something that matches your desired UI query.
Remember that the desired thought model for users of Fulcro is that the db is literal, the queries hit the db, and rendering is simply a F(data) where F(data) = Render(Pull(Query, data)). There are no weird things “running on the side” in the View logic. It’s as pure as possible, and as lightweight as possible. ALL of your reasoning about manipulation of data should happen in a mutation or a state machine or a statechart.
Note: Since functions like sort/filter/map on pure data always result in referential transparency, the choice to do these kinds of operations in the UI itself it merely a matter of optimization. Are they cheap enough to run in the UI without harm? If so, fine. If not, reify their result into the db with a mutation.
Fulcro itself also does not define any kind of reactive data derivation (ala re-frame), but you could add such a thing if you felt the desire. In my apps I’ve not really felt the need to add the complexity of such a system, and prefer the more direct reasoning about the data and data flow that I get with Fulcro.