Fork me on GitHub
#fulcro
<
2021-01-28
>
yubrshen05:01:00

Not able to achieve the only rendering of person 1 At Fulcro 3 Part 5 How Rendering Works, around 13:25, Tony showed that only “Render person 1” with the default (ident-optimized-render), when executing:

(comp/transact! APP [(make-older {:person/id 1})])
But Using the exact copy of Tony’s code from https://github.com/fulcrologic/video-series/blob/how-rendering-works/src/app/client.cljs I still got the following:
Render root
client.cljs:59 Render list
client.cljs:33 Render person 1
I also tried to explicitly use the ident-optimized-render with the following code, which is almost identical to Tony’s, but still got the same rendering of more than the person 1’s.

yubrshen05:01:00

(ns app.client
  (:require
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom :refer [div ul li h3 label]]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [com.fulcrologic.fulcro.rendering.keyframe-render :as keyframe]
    [com.fulcrologic.fulcro.rendering.ident-optimized-render :as ident-optimized]
    [com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]))

(defsc Car [this {:car/keys [id model] :as props}]
  {:query [:car/id :car/model]
   :ident :car/id
   :initial-state {:car/id :param/id
                   :car/model :param/model}}
  (js/console.log "Render car" id)
  (dom/div
   "Model " model))

(def ui-car (comp/factory Car {:keyfn :car/id}))

(defsc Person [this {:person/keys [id name age cars] :as props}]
  {:query         [:person/id :person/name :person/age {:person/cars (comp/get-query Car)}]
   :ident         :person/id
   :initial-state {:person/id :param/id
                   :person/name :param/name
                   :person/age 20
                   :person/cars [{:id 40 :model "Leaf"}
                                 {:id 41 :model "Escort"}
                                 {:id 42 :model "Sienna"}]}}
  (js/console.log "Render person" id)
  (let [onClick (comp/get-state this :onClick)]
    (div :.ui.segment
         (div :.ui.form
              (div :.field
                   (label {:onClick onClick} "Name: ")
                   name)
              (div :.field
                   (label "Age: ")
                   age)
              (dom/button :.ui.button {:onClick (fn []
                                                  (comp/transact! this
                                                                  `[(make-older ~{:person/id id})]
                                                                  ))}
                          "Make Older")
              (h3 {} "Cars")
              (ul {}
                  (map ui-car cars))))))

(def ui-person (comp/factory Person {:keyfn :person/id}))

(defsc PersonList [this {:person-list/keys [people]}]
  {:query [{:person-list/people (comp/get-query Person)}]
   :ident (fn [] [:component/id ::person-list])
   :initial-state {:person-list/people [{:id 1 :name "Bob"}
                                        {:id 2 :name "Sally"}]}}
  (js/console.log "Render list")
  (div :.ui.segment
   (h3 :.ui.header "People")
   (dom/ul
    (map ui-person people))))

(def ui-person-list (comp/factory PersonList))

(defsc Root [this {:root/keys [list]}]
  {:query [{:root/list (comp/get-query PersonList)}]
   :initial-state {:root/list {}}}
  (js/console.log "Render root")
  (dom/div
   (dom/h3 "Application")
   (ui-person-list list)))

(defonce APP (app/fulcro-app {:optimized-render! ident-optimized/render!}))

(defmutation make-older [{:person/keys [id]}]
  (action [{:keys [state]}]
          (swap! state update-in [:person/id id :person/age] inc)))

(defn ^:export init []
  (app/mount! APP Root "app"))

(defn get-components-that-query-for-a-prop
  [prop]
  (reduce
   (fn [mounted-instances cls]
     (concat mounted-instances
             (comp/class->all APP (comp/registry-key->class cls))))
   []
   (comp/prop->classes APP prop)))

(comment
  (comp/transact! APP [(make-older {:person/id 1})])
  (get-components-that-query-for-a-prop :person/age)

  (def before (app/current-state APP))
  (def after (app/current-state APP))

  before
  after

  (map
    comp/get-ident
    (get-components-that-query-for-a-prop :person/name))

  (let [state           (app/current-state APP)
        component-query (comp/get-query Person)
        component-ident [:person/id 1]
        starting-entity (get-in state component-ident)]
    (fdn/db->tree component-query starting-entity state))

  )

yubrshen05:01:00

The only difference may be is the version of Fulcro. I’m using

com.fulcrologic/fulcro {:mvn/version "3.4.14"}
What could be the issue? Thanks!

yubrshen05:01:59

Once I reached the end of Part 5, after adding the display of the count for over 30, and :refresh, then the behavior with clicking on the button of "Make Older" is the same as shown in the video.

Jakub Holý (HolyJak)13:01:13

In 3.4 the default renderer was changed from the ident-optimized one to the multi-root one which is not ident optimized and always renders from root, i.e. checks the props of all ancestors of an affected component for changes and renders them if changed. Most of the time the decreased complexity is worth the negligible performance cost of that.

🙏 3
yubrshen05:01:59

Once I reached the end of Part 5, after adding the display of the count for over 30, and :refresh, then the behavior with clicking on the button of "Make Older" is the same as shown in the video.

nivekuil06:01:19

has anyone thought about per-element activity markers? e.g. click a link/button and it turns into a spinner while waiting for a corresponding load/mutation. I was thinking about hooking into the tx processing stuff, but that might be gross and run afoul of f: state -> ui

Thomas Moerman10:01:22

I think there is a problem with eb/error-boundary and localized css

Thomas Moerman10:01:21

the :.y class name is expanded in function of the error boundary component instead of the containing component with the {:css [[...]]} section

(mui/grid {:item true
           :xs   12
           :lg   6}
  (err/error-boundary
    (comp/fragment
      (div "bla")
      (styled-paper :.y {}
        (mui/typography :.p
          {:paragraph true
           :color     :textSecondary}
          
          (dom/i "TODO search, filter...")

          )))))

Thomas Moerman10:01:27

does this make sense?

Jakub Holý (HolyJak)13:01:13

In 3.4 the default renderer was changed from the ident-optimized one to the multi-root one which is not ident optimized and always renders from root, i.e. checks the props of all ancestors of an affected component for changes and renders them if changed. Most of the time the decreased complexity is worth the negligible performance cost of that.

🙏 3
Jakub Holý (HolyJak)13:01:11

I am trying to answer this question: When do you need to define :initial-state? My answer so far: 1. When you want to make sure that the component has particular props before any data is loaded from the backend 2. When the component has no state of its own and only queries for global data using {url-book}#linkqueries[Link Queries] 3. When a child component has initial state (f.ex. dynamic routers do) 4. (perhaps) When the component is used as a target of a dynamic router Thoughts? 🙏

tony.kay16:01:03

The answer is very simple: When the query needs to reach a particular part of the UI graph from the first render frame of the app. The query has the shape of the UI, but it is really a standalone API: The first render is literally passed the props of (db->tree (get-query Root) state state). Link queries dangling off the “edge” of the real data are never fulfilled because db->tree stops evaluating the query when an edge isn’t found. There’s nothing else it can do. An edge could be to-one, or to-many…how is it supposed to “invent” the missing edges?

👀 3
Jakub Holý (HolyJak)17:01:38

If I understand your response correctly then it conforms the 3 (4) points I wrote above. Right? In particular 2 shows where the UI cannot be reached without the help of initial data.

Jakub Holý (HolyJak)17:01:57

I do not really understand what "Query reaching a part of the UI" means. Query just is, it doesn't do anything. I guess you mean the process of fulfilling the query from the client DB reaching (and not skipping) a particular part of the query? I also do not comprehend "Query being an API". Sorry 😢

tony.kay18:01:32

Sorry, yeah, db->tree is the query API. It runs the query against the db. When it reaches a join that has no corresponding real data then it HAS to stop because there is no info on how to spring such an edge into existence. So, if there is a “link query” deeper in the query from there, it will not been realized, because there is no way to reach it. (sure, it comes from the root, but it’s dangling off in space in the reified graph, so it simply cannot make it’s way to the UI that destructures the result from root). So, that’s what I mean by “query reaching UI”. Your intention when you write a compoentn-local query is that the query processing will “reach the point and fulfill your request”, but if you didn’t connect the graph, then it cannot reach the query that is associated with that part of your UI.

👍 3
JAtkins20:01:12

so, if the tree is

a -> b -> c
and the query is
[:a/id {:a/child [:b/id {:b/child [:c/id]}]}]
and (a,c) have initial state you care about, you must have initial state on b. otherwise your state will look like:
{:root/a 1
 :a/id {1 {:a/id 1 :a/child nil}}}
and db->tree halts, even though the query continues

👀 3
👍 6
JAtkins20:01:13

I know that's what you said in #3, but this rule kind of encompass all of your 4 rules.

Jakub Holý (HolyJak)22:01:56

Thanks a lot for the explanation, Tony!

Aleksander Rendtslev13:01:27

Super exciting guys! I’m so impressed with everything you’re putting together. Big shoutout and a thank you for all your amazing work https://www.fulcrologic.com/copilot

Jakub Holý (HolyJak)14:01:56

@U0CKQ19AQ I guess the word "tool" is missing from ☝️ after "great" in: > We use them every day in production environments and find them to be a great for solving ?

tschady14:01:02

are there any fulcro friendly tools for First Run Experience?

Jakub Holý (HolyJak)14:01:35

What is First Run Experience? What kind of tools do you have on mind?

tschady14:01:30

e.g. an interactive overlay to walk you through how to use an interface for the first time. like graying out most of a UI, but highlighting one area with “Click here to manage your account”, then you click next and it will highlight the next thing. mini-tutorial for first-time users.

👍 6
tschady14:01:21

i thought i had seen some tooling for other web frameworks (like Rails) to manage this with ease but now I can’t seem to find any.

Jakub Holý (HolyJak)14:01:01

I have not heard about anything like that for Fulcro

Jakub Holý (HolyJak)16:01:35

I would appreciate help with my Fulcro tutorial. Is the text and diagram below clear? Useful? Can it be improved? > The image below shows how the query fragments of all components are composed into the Root component’s query and sent to the backend which responds with a tree of data, which is then propagated down from Root to its children and so on. (We omit the role of the client DB here for simplicity.)

nivekuil16:01:27

I would appreciate the network lines being colored correspondingly, and the data line pointing to the root node directly. also not sure why there's 2 triangles or why some shapes are outlines and some are solid

👍 3
bmaddy17:01:02

I love where you're going with this. I also didn't realize right away that the shapes of the component tree on the left correspond to their queries. Have you tried sketching out a version where each component is a box and their queries are yellow shapes inside them? Using shapes instead of keywords is great!

😻 3
bmaddy17:01:28

I suspect that would help make it clear that the white shapes don't have queries (if that's correct).

3
JAtkins20:01:21

Yes, this is great... My only issue is that the final map {triangle} has only one element. it won't compile 🙂

😂 3
Jakub Holý (HolyJak)22:01:34

Thank you all, great suggestions!

Buidler23:01:50

Looks good, you are making some amazing contributions to the Fulcro community lately @U0522TWDA. As a small suggestion, you may want to consider doing the diagrams in something like https://github.com/mermaid-js/mermaid as it lends itself better to version control and alleviates the need to think about styling as much.

❤️ 3
👀 3
Jakub Holý (HolyJak)08:01:10

Mermaid looks cool but it is not powerful enough for this case (adding the query and data arrows to the UI tree,.. 😭

Jakub Holý (HolyJak)10:01:52

I have updated the figure as suggested. Any further comments? Thank you all!

🙌 3
4
tony.kay17:01:22

I have one other comment: The query/return of data is not tied to root. The query has a default target of a root key based on a subtree query, which can be re-targeted. This is not an insignificant point. You never really query from root.

Jakub Holý (HolyJak)18:01:18

Right, you don't do (df/load! :all-data Root) , you do something like (df/load! :all-projects Project) where Root shows a list of projects and has query like [.. {:all-projects (comp/get-query Project)}] . Is that what you are saying? Now I need to think hard how to make that clear in the figure without just confusing everybody... So the Root query is used to fetch data from the client DB and build props and send them down to Root but it is never used to load the data.

tony.kay18:01:49

right….never is a strong word, but in real apps that is true

tony.kay18:01:34

you’re always loading (or merging) some targeted subtree

tony.kay18:01:00

so, I’d draw the SERVER query from some child node and merge back to that node

tony.kay18:01:18

where that child might be dashed in some cases (doesn’t exist in graph yet)

tony.kay18:01:26

the UI query is from root

tony.kay18:01:56

so, if you change the graph to say “UI Client DB” on the right, then that diagram is absolutely right

tony.kay18:01:29

but for merge-component and server interaction the in/out lines should connect to a sub-portion of the left graph, not the root

Jakub Holý (HolyJak)19:01:04

Genius! I will do just that, replace Pathom with Client DB

tony.kay00:01:43

yeah, now all you need is the picture of the graph in the client db, and to the right of the client DB another query out, data in that targets a portion of the sub-graph.

Jakub Holý (HolyJak)11:01:47

Hm, I am tempted to leave it as is and not discuss that part :-) But we can give it a try. Any thoughts about how to visualize the graph in the client DB and the query<>data of a subtree?

nivekuil12:01:51

maybe some inspiration from the diagrams here? https://tonsky.me/blog/the-web-after-tomorrow/

👀 3
yubrshen17:01:17

@U0522TWDA really appreciate your help for us to understand Fulcro! I wish to have some more narrative to explain the diagram in your tutorial. To the minimum, explanation of the legends, such as, the meaning of those icons of solid circle, square, and triangle, etc. (they look intriguing.) Another aspect, please explain the cause-effect relationships of those links in the graph, what happen first, what are the consequence of that action, etc. (I did check your tutorial, it's a pity that without more narrative, your wonder diagram might not reach its fuller impact.)

❤️ 3
Jakub Holý (HolyJak)09:02:33

Hi @U2QGRCMSM! Thank you. I have now added a simple legend to the figure. If you read the paragraph just above it, it explains the causal sequence. See https://github.com/fulcro-community/guides/blob/main/minimalist-fulcro-tutorial/index.adoc#components-query

dgb2316:01:55

When looking at this I’m trying to picture an animation in my head. It’s certainly useful. An improvement could be a series of steps to kind of see how the tree is walked maybe.

Jakub Holý (HolyJak)09:01:30

Great idea, an animation would be awesome. Only I have no idea how to make it 😞

dgb2312:01:23

An easy and low effort way is to draw the pictures in a series and leave them statically. Another possibility is to draw them as SVG use CSS transitions and a bit of JS to trigger them. If the latter is an option I’m happy to help!

Jakub Holý (HolyJak)15:01:13

I do not know how I would do that. But please give it a try, if you want, the SVG is here https://github.com/fulcro-community/guides/blob/main/minimalist-fulcro-tutorial/fulcro-ui-query-data.svg

dgb2315:01:07

I’m in a hurry right now, but I could try next week. However a prerequisite would be to use github pages so you can include arbitrary CSS/JS. I don’t know if this is out of scope for this tutorial?

dgb2315:01:02

Let me first send you a prototype next week when I have time and then we go from there!

👍 3
Jakub Holý (HolyJak)15:01:02

No, I think it would be a good idea to create GH pages for the fulcro-community guides.

dgb2313:02:19

Hey I just came up with a very simple idea that is easy to implement: would it be useful to convey the steps with simple highlighting? So each step highlights both a node in the structure (left) and a related part in the EQL literal (right)? I think that might already convey very much without too much effort!

dgb2313:02:51

If you think this is a good idea I will make it and send it tomorrow.

Jakub Holý (HolyJak)16:02:22

If you are sure it is worth the effort, go for it!

👍 3
dgb2322:02:32

not sure if it was worth it 😄

dgb2322:02:41

if you think it’s useful I can also improve it a bit. Idk!

dgb2322:02:58

it’s not much work

Jakub Holý (HolyJak)09:02:47

I just realized that to be able to actually use it, we will need to move from using GitHub's .adoc preview to GitHub pages because I think the former does not support custom javascript 😢 . It was the plan anyway but I have very little time so it will take a while 😭

👍 3
henrik16:01:10

I'm trying to refresh a parent from the context of a child, like this:

(comp/transact!! child-this `[(m/set-props ~{:child/value value})] {:compressible? true
                                                                    :refresh [[:parent/id parent-id]]})
The parent doesn't refresh (as verified by turning on "highlight updates" from react devtools). Am I using this correctly? Renderer is com.fulcrologic.fulcro.rendering.keyframe-render2.

Björn Ebbinghaus17:01:29

The keyword is: :only-refresh

henrik22:01:53

Same result. I understood :only-refresh as being the desired option when you want to override the default refresh (I.e., the calling component), rather than add to it. Is this a misunderstanding?

Jakub Holý (HolyJak)07:01:50

transact!! is special and only refreshes the this component I believe. Cannot you use transact! ?

henrik11:01:36

transact!! just merges {:synchronous? true} into the opts of transact!, and being able to target refreshes is specifically interesting with synchronous transactions, since they only refresh the caller (by default).

henrik11:01:03

Yeah, so refresh/`only-refresh` seems to be for non-syncronous transactions only. Since synchronous is recommended for value transactions to an input, I'm not sure how to make a parent aware of the input change interactively, when this is necessary.

henrik11:01:43

Running comp/refresh-component! seems to be an inelegant, but effective, workaround.

Chicão19:01:00

Hi, I want connect fulcro +pathom and datomic, but I don't want use RAD template, if I change here with a datomic (d/db conn) my parsers from pathom will have access to datomic and will be able to make a pull query ? May someone has an template to share if mee I will be grateful.

Jakub Holý (HolyJak)07:01:16

start from the RAD template and delete all the RAD stuff. Inline the code that creates app from RAD. That is likely to work.

👍 3
Buidler23:01:16

This copilot thing sounds like it has some overlap with Guardrails.

currentoor00:01:27

i believe it's meant to be the pro paid version of guardrails

currentoor00:01:53

similar to how intelliJ has a free lite version and a pro paid version

tony.kay01:01:17

You code in Guardrails. Copilot uses the same guardrails info to then do code analysis while you’re editing (instead of needing you to run the code)

tony.kay01:01:57

So, copilot is an add-on tool with IntelliJ integration that is sorta like clj-kondo, but understands the data better, so it can find more interesting errors.

🆒 3
Buidler12:01:28

Interesting. I had been wondering if there is some potential for Guardrails and clj-kondo to be friends.

tony.kay19:01:29

kondo is stricyly a static source analysis in a pre-compiled executable meant to give you Cursive-like feedback on your source in real-time. A dynamic analysis requires a running runtime using your code, and is much slower. So, it has different pros/cons. A static analysis can choose to tolerate syntax errors and other ills in order to give you structural feedback. It can also do some limited localized code comprehension to give you some help. Dynamic analysis needs your code to run in running order and has the same dynamic env drawbacks as REPL usage (load order, reloading dependencies, etc.). So, it is slower and more difficult to use; however, dynamic code analysis can give much broader coverage for finding regressions, cross-code behavioral problems, etc.

tony.kay19:01:13

They are actually complimentary.

tony.kay19:01:58

CLJ-kondo - static structural problems and some localized errors Guardrails - Real runtime data flow detecting problems against real use-cases (requires active usage) Copilot - Leverage generated data and “pretend” to run your code to detect the things that Guardrails MIGHT find if you were to run your project enough times. Can actually do a lot more than GR, but the “runtime” mode of GR is also complimentary. Unit Tests - Test specific cases and behaviors you want to prove right Generative tests - Exercise algorithms under some more specific external constraints/proofs. Integration Tests - Simulate real full-stack system use. Finds integration problems and bugs.

tony.kay19:01:08

IMO they all bring something of good value to the table. They just each have different pros/cons. More tools in the toolbox, so to speak.

Buidler23:01:51

Thanks for the detailed explanation @U0CKQ19AQ.