Fork me on GitHub
#fulcro
<
2018-07-06
>
grant15:07:06

Is there a way to make ident queries dynamically? (i.e. To pass the id of the entity that you want to do an ident query from.) Or is the "correct way" to use link queries and just have root properties with the entity or entities of interest in them?

currentoor17:07:04

@grant dynamic queries are supported, but for that why can't you just use df/load?

tony.kay17:07:22

@grant It is unclear what you’re asking…do you mean loads (get from the server) or queries (get a join query based on an ident) or function calls to get idents from ids?

grant17:07:09

Maybe I am thinking about things wrongly. I have some components that are purely for data, they have no render function. And I have some UI components that query and need to get pieces of those data components.

grant17:07:01

Basically I was trying to separate the domain model for the UI model by having separate components. That may not be a good idea though.

grant17:07:17

Right now I am doing everything client side. I have no server interaction yet.

grant17:07:02

The final nesting and location of some of the UI components is not well understood at this point. But I have a decent grasp over the domain model, I think. So I was trying to build UI components that could be told what data component they were pulling their data from at the moment.

grant17:07:40

(e.g. The data attributes for all the parts in a catalog would be normalized separately from all of the UI component attributes.)

tony.kay17:07:33

@grant That is an ok thing to do…as long as the idents match between the components

tony.kay17:07:28

I usually write a support function for the idents in that case, and use them from all related components (and include two arities for convenient use in mutations)…thus I typically call it something-path:

(defn component-path 
  ([id] [:component/by-id id])
  ([id field] [:component/by-id id field]))

(defsc Component [t {:keys [id]}]
  {:ident (fn [] (component-path id)) ...})

(defsc SomeQueryOnlyComponent [t {:keys [id]}]
  {:ident (fn [] (component-path id)) :query ...})

(defn component-op* [state-map id param]
   (assoc-in state-map (component-path id :some-field) (f param)))

(defmutation component-op [{:keys [id some-param]}]
  (action [{:keys [state]}]
    (swap! state component-op* id some-param)))

tony.kay17:07:36

then it doesn’t matter how many UI concerns map to the same underlying data….it is all generalized to that path function.

tony.kay17:07:08

And of course loads can then use any component with that ident to fetch data…but of course they will only update the portion of the data they query for, which may or may not be acceptable (could put your data in a state that is impossible from the perspective of a server)

grant18:07:06

Only loading what is needed was one of the things I was going for. There is too much data to load everything for all of them. I think I will end up needing a couple attribute on a lot of them and most of the attribute on the few that a user is working with in detail.

tony.kay19:07:12

@grant Another thing to consider is treating the schema of this large data differently only the client than it exists on the server, and using something like pathom to translate between the two.

tony.kay19:07:51

That would be my recommended approach. Much cleaner to let the client UI ask for what it needs in an localized (perhaps even renamed) sense

tony.kay19:07:02

and let the sever send just what is requested for that UI component

tony.kay19:07:25

it’s a stellar combination

tony.kay19:07:16

I talk a bit about it here in this video (at this time offset): https://youtu.be/gbrdnSsUerI?t=24m19s

grant19:07:03

Just now I was experimenting with flattening things out, based on your component path example.

tony.kay19:07:38

I wouldn’t flatten unless that “makes sense”. The ideal in the client is to have the structure that the UI tree prefers.

tony.kay19:07:10

and to use a transform (or tool like pathom) to convert what you have on the server into that shape

grant19:07:33

I mostly want to come up with something that scales well and stays relatively clean and understandable. I tried this with Re-frame and ended up making a mess that was hard to follow once it got big.

tony.kay19:07:46

that’s always the challenge 🙂

grant19:07:35

I really like the idea of being able to peel off subsets of the app and still have them work.

tony.kay19:07:40

So, remember these core things: Mutations should be high-level abstractions, esp when full-stack. They should not be about shape, but about change. Queries are about hydrading a UI, and of necessity include shape (your desired React UI tree dictates that to some extent).

tony.kay19:07:13

The transform step between that UI query and the server model is the critical bit, and pathom makes it easier to write

tony.kay19:07:38

and mutations as abstract operation symbols isolate your “full stack” logic from that shape as well

tony.kay19:07:00

Assuming you don’t try to bit-twiddle

tony.kay19:07:14

as soon as you try to bit twiddle, you are back to complexity

grant20:07:20

Maybe I will try to take a step back and try to ignore my knowledge of the backend, see if I come up with a structure that feels simpler for the front end. One of the other tradeoffs I have been trying to find balance with, is where to draw the line with things like specific components vs generic components. (e.g. (defsc NameForm ...) (defsc SerialNumberForm ...) vs. (defsc TextInputFrorm [t {:keys [entity-attr ..]).) Any advice there?

wilkerlucio20:07:46

@grant depends on how you want to to use it, for basic things I like to have then stateless (no query, no ident, just get props) and then you can wrap it with data around, this way you can have different data sources and still use the same component

currentoor21:07:10

unless you need the lifecycle callbacks, why even make it a component at that point?

currentoor21:07:35

would a function that returns react dom elements work fine?

wilkerlucio14:07:02

in my case a lot of times is about css 🙂

tony.kay20:07:29

@grant it just depends on the case. I tend to not prefer swiss-army-knife things. General-purpose is fine. E.g. the forms state management namespace is about keepign a copy of data and giving you diffs...it's reusable, but not doing "more than one thing" Making TextInputForm might be either, but I think it might fall too much on the "too many things in one box" category.

tony.kay20:07:06

If all your forms look the same, have similar inputs, and could be easily configured with data, then sure, it might make sense to make a form renderer that can be configured via data. It also make sense if your data is dynamic (in fact, required in that sense), but the complexity of that kind of generalization can be quite costly. It's super-cheap to write small renderers to do things like render a text field and then compose that into each individual form, which in turn is really simple.

currentoor22:07:13

So I'm making a POS app for iPads, it's React Native but the UI is entirely a full screen WebView component running Fulcro. I'm able to send messages bi-directionally between native code and the webview. I need this for example when a user clicks a Pay button, I need the native code to talk to a credit card reader.

currentoor22:07:45

Right now it's done with callbacks and promises. Should it be an actual remote instead?

tony.kay22:07:41

really up to you.

tony.kay22:07:47

if it seems like wrapping queries and mutations around it help...external interfacing with side effects is often nicer behind such a thing

currentoor22:07:02

right now my mutations look like this

(m/defmutation send-to-native-land [{:keys [counter button-color]}]
  (action [{:keys [state]}]
    (let [new-counter   (inc counter)
          new-btn-color (if (= "green" button-color) "red" "green")
          send-fn       (:send-fn webViewBridge)]

      (send-fn "handleDataReceived" new-counter
               ;; On Success
               (fn [res]
                 (swap! state (fn [s]
                                (-> s
                                    (update-in [::bridge-example :singleton] assoc
                                               :counter new-counter
                                               :button-color new-btn-color)))))
               ;; On Error
               (fn [err]
                 (js/alert (str "Error: " err)))))))

currentoor22:07:54

i almost certainly won't be sending queries over this bridge, the webview component does XHR and websockets fine

currentoor22:07:25

@tony.kay is it bad to mutate app state in the on-success callback of something like this?

currentoor22:07:09

potentially refresh checks could happen out of order right?

tony.kay22:07:49

ideally you'd jus transact against the reconciler

tony.kay22:07:19

no real problem with that