Fork me on GitHub
#fulcro
<
2019-02-19
>
hmaurer01:02:25

Do you ever use hash as a React key where there is no more sensible choice? e.g. (prim/factory MyComponent {:keyfn hash})

pvillegas1203:02:45

With dr/route-deferred can I issue a call to prim/transact! and do (dr/target-ready* target) in the mutation?

tony.kay05:02:59

@hmaurer don’t do that. key should be stable. See React docs. You only need a key if that component will appear in an array with siblings of the same type, in which case you can use map-indexed or ids

hmaurer11:02:51

Hmm I am aware of the react doc on the topic, but a hash of the data seems more stable to me than an index :thinking_face:

tony.kay17:02:26

the data changes in ways that don’t “move” the component

tony.kay17:02:25

(ui-x {:x 1}) -> (ui-x {:x 2}) should NOT unmount and remount the component…it should update the props

tony.kay17:02:47

setting the key to a hash of that data WILL force it to unmount and remount, and a hard DOM flush…very very bad for performance

tony.kay17:02:04

so, the only thing worse you could do is set it to rand-int

hmaurer17:02:22

Ah, I see what you mean. Fair point; thanks for the explanation 🙂

tony.kay17:02:35

sure…you only need key if the component instances will appear in an array/sequence…they can be hard-coded siblings. The key is for places where React detects that the list of things can change over time

tony.kay17:02:12

that’s why adjacent divs don’t need them

tony.kay17:02:25

but anything output from map does

hmaurer18:02:30

@tony.kay oh yes that I know. I was only considering using hash as a key for sequences, instead of an index

tony.kay05:02:04

@pvillegas12 That is the intended usage, yes, but I think I see a bug in the source…that will not move you to the new correct state in that router’s state machine 😕

pvillegas1212:02:33

Doing a (uism/trigger! reconciler router-id :ready!) in the mutation seems to transition the state machine correctly, but I’m not sure if doing

(let [router-id (dr/router-for-pending-target @state target)]
      (uism/trigger! reconciler router-id :ready!)
      (swap! state
        (fn [s]
          (-> s
            (target-ready* target)
            (assoc-in ...)))))))
is the way to go

tony.kay17:02:55

no, not very clean

tony.kay17:02:05

you shouldn’t need to know anything about that

pvillegas1212:02:48

@tony.kay I’m also hitting another roadblock with dynamic routing. When I change route, target-ready* will side-effect only when it can find the router-id in the normalized database. However, when I trigger the route change, the state will not have the :fulcro.incubator.dynamic-routing/pending-route part yet and it will not transition. Clicking on the link again works as the pending-route part is now in the db.

tony.kay14:02:20

@pvillegas12 if you use a deferred route you should never immediately mark the target ready. But, this is a similar issue as above and in the issue: The state machines for all routers should start on app start.

pvillegas1216:02:39

This seems to work

:will-enter      (fn [reconciler {:keys [model-id]}]
                      (let [target [:model/by-id model-id]]
                        (prim/transact! reconciler
                          `[(mut/add-model ~{:target         (conj target :model/form)
                                             :form/component Model})])
                        (dr/route-immediate target)))
 
Not sure if it respects the no side-effecting on the :will-enter hook

tony.kay17:02:09

I see that I put on the doc string that it should be side-effect free, but I don’t remember why. Oh, because will-enter will be called from within a running mutation. So, technically, you can do this if you use ptransact! (which defers its transaction). Calling transact inside of transact usually “does what you want”, unless the operation in question was called from within a swap! on the state atom, in which case weirdness will ensue.

tony.kay17:02:17

Thanks for sticking with the new router…I think it is a big improvement, and it’ll be nice to get these kinks worked out of it.

pvillegas1214:02:48

Can I call a transaction with the immediate call?

tony.kay17:02:20

@pvillegas12 See https://github.com/fulcrologic/fulcro-incubator/issues/28. I mean for you to use deferred in that case, but it seems like there is a bug that keeps that from working correctly. The workaround is to use ptransact!. transact! “may” work, but it ends up running the new transaction “nested”, which isn’t technically safe.

pvillegas1221:02:58

The work-around is using ptransact! within within the will-enter hook and calling (dr/route-immediate target) right? My use case in particular is building a model when a route is entered. I can resolve the route immediately, but I want to perform a mutation corresponding to building the model.

tony.kay21:02:11

technically I think route-deferred is what we want, with a target-ready* inside the mutation, but that helper isn’t right

pvillegas1221:02:13

Correct, route-deferred should be able to receive a mutation calling target-ready*, I agree.

nha22:02:32

I am looking at the form support. I am trying to make a simple client-side form, and I don’t really get what I should put in my :query and :ident. Filling the form is client-side only, and I guess I will have to make a mutation on the submit event.

(defsc SubscribeForm [this {:keys [channel] :as props}]
  {:query [:ui.sub-form/id      ;; not sure what that will be, but what would I put as ident otherwise?
                :ui.sub-form/channel]
   :ident [:ui.sub-form/id] ;; what will fill the ID there?
   :initial-state {:channel "#demo"}
   :form-fields #{:channel}}
  (dom/div
    (dom/h3 "PubSub Form")
    (dom/form
      (dom/input {:type "text"
                           :value channel   ;; is `nil` here - why since I have an initial-state?
                           :required nil})
      (dom/input {:type "submit"
                  :value "submit"}))))

pvillegas1222:02:27

I use [:model/by-id :db/id] as ident in those form components

tony.kay22:02:21

if it is a single-use thing that has no ID, just make up an ident like :ident (fn [] [:FORM/by-id ::subscribe]), nothing needed in query for the ID since you have none. But you must include the form config in your query

nha22:02:23

What is the difference between fn idents and vectors idents?

tony.kay22:02:46

vector-based uses first ele as literal, second to pull from props

💡 5
tony.kay22:02:08

fn form lets you use props from arg list to do it however you want, including just sending a constant ident

tony.kay22:02:16

or calculating it somehow

tony.kay22:02:26

arg list of defsc

5
pvillegas1222:02:57

@tony.kay for some reason, on the component

(defsc-route-target CompanyDetails
  [this {:keys [] :as props}]
  {:ident           [:company-details/by-id :company-id]
   :query           [:db/id :company-id]
   :route-segment   (fn [] ["empresa" :company-id])
   :route-cancelled (fn [_])
   :will-enter      (fn [_ {:keys [company-id]}]
                      (println "CompanyDetails:" company-id)
                      (dr/route-immediate [:company-details/by-id company-id]))
   :will-leave      (fn [_] (js/console.log "Leaving CompanyDetails") true)}
  (dom/div {:onClick #(my-mutation this)})
 )
I’m calling (prim/get-ident comp) on a mutation used by this component and getting nil for company-id even if I am getting hte correct id in the will-enter hook.

pvillegas1222:02:14

Also getting the warning

pvillegas1222:02:28

component dataico.ui.company.details/CompanyDetails's ident ([:company-details/by-id nil]) has a `nil` second element. This warning can be safely ignored if that is intended.
when I trigger the route change

tony.kay22:02:50

what is comp?

tony.kay22:02:59

a class or instance?

pvillegas1222:02:03

in this case the instance of CompanyDetails

pvillegas1222:02:16

calling (my-mutations this) and the implementation calls (prim/get-ident this)

tony.kay22:02:17

sure? Not the class, but a true react mounted instance?

tony.kay22:02:47

the only reason you get that is if that component has empty props, basically

pvillegas1222:02:17

mmm, as it is a defsc-route-target, where should I get the props from?

tony.kay22:02:26

which is happening because your route is going there before your mutation puts the data in place

tony.kay22:02:58

which is why you need deferred in the first place 🙂 So, our workaround won’t work…you’ll need to use deferred with ptransact!

pvillegas1222:02:16

Actually, I made it so that I would build the model after clicking on a button

tony.kay22:02:35

the props aren’t in state on that render, which is why you’re getting the warning

pvillegas1222:02:05

I’m confused now, currently doing

:will-enter      (fn [_ {:keys [company-id]}]
                      (println "CompanyDetails:" company-id)
                      (dr/route-immediate [:company-details/by-id company-id]))
after going to that route, I would expect the fulcro db to show [:company-details/by-id { company-id: []}].

pvillegas1222:02:13

Or are you saying I should make a mutation which creates that ident manually? Something like (assoc-in state-map [:company-details/by-id company-id] { ... })?

pvillegas1222:02:23

Basically, I have a list of companies in the database. Clicking one (which is already loaded in the normalized db) routes to a company details page. As I already have it in memory, I’m not doing any loads or deferred, just immeadiately routing there.

Joe Lane22:02:03

Alright friends, I’m back again. So I’m trying to use the aws js cognito sdk. Last time I showed up I got the impression that a remote was the place to call that auth code. Turns out that remotes are only for network operations now, meaning that the “remotes as an abstraction” blog post must be no longer true. Where is the proper place in fulcro to put the client side auth code to interact with cognito?

tony.kay22:02:56

@pvillegas12 ah, well, then there should be props

tony.kay22:02:09

so, I’m confused as to how you’re seeing that warning

tony.kay22:02:13

but that’s what it means

pvillegas1223:02:26

Going back to the basics, I was pointing at the incorrect ident in the fulcro database, that’s why it could not find the id correctly. I was doing [:company-details/by-id :company-id] instead of [:company/by-id :db/id].

tony.kay22:02:57

@joe.lane nothing is out of date about that blog post, other than there might be newer ways of building a remote

tony.kay22:02:06

don’t remember when I wrote it

tony.kay22:02:41

Yeah, there are better ways of making a remote now

tony.kay22:02:51

client supports middleware

Joe Lane22:02:10

So, this remote basically wont be hitting my backend server

Joe Lane22:02:26

instead it will be routed to cognito (third party)

Joe Lane22:02:34

Is that still a concern of middleware?

Joe Lane22:02:42

Ok thanks, I’ll take a look again.

tony.kay22:02:45

do you need to “redirect” the page?

Joe Lane22:02:51

No, its a SPA

Joe Lane22:02:03

Well, once authed, I redirect

tony.kay22:02:03

so you’re rendering the login form

Joe Lane22:02:39

Yes, I’m doing a passwordless login via sms. There are 3 different login component phases

tony.kay22:02:40

so it is typicaly oauth kind of thing: You go to remote server to log in, and it redirects to your app

tony.kay22:02:08

so at that point you have what? A cookie, a JWT token that came in params, what?

Joe Lane22:02:34

at that point I just need to call the cognito sdk and it is handles that for me

Joe Lane22:02:47

it shoves the jwt token in localstorage

Joe Lane22:02:57

and I never look at it again

Joe Lane22:02:16

My code originally was tangled up in mutations which I figured wasn’t the right place since it isn’t a reproducible transaction

Joe Lane22:02:52

Effectively I’m just trying to call the cognito library using the right mechanism and code organization in fulcro. Next though I’ll be doing the same thing but with s3 file uploads using the same sdk, so I want to make sure I’ve got a good understanding of the problem. Clearly I’m missing something.

tony.kay22:02:55

so, you have a js-land lib that does that for you?

tony.kay22:02:15

I’d put that call in started-callback/client-did-mount

tony.kay22:02:33

do you then need to pull data from there and put it in fulcro state?

tony.kay22:02:29

I mean, this is a “startup concern” as far as I can tell

Joe Lane22:02:31

Yeah, I believe so

Joe Lane22:02:57

I have 3 fulcro dynamic routes that I use for signing up users.

Joe Lane22:02:57

I wouldn’t consider it a startup concern if the fulcro app is used in the sign up process.

tony.kay22:02:16

If you need it in initial state, you could also then transact it into state during startup. If you need to “redirect” them if they haven’t yet signed in, you might even delay loading the app until you see they have good creds (in js land, with exported functions). I did an app with oauth, and the “landing page” wasn’t even the real app. It was a very light js page that checked for creds and did a browser redirect to the real app or login. This kept the first page load really fast

tony.kay22:02:46

but the point is you’ll never get the “creds” without a real redirect to a remote page the fulfills the auth, right?

tony.kay22:02:32

sry, I don’t have time to read that

Joe Lane22:02:43

Its not a page redirect

tony.kay22:02:01

ok, so the js lib is capable of doing it all, I see

Joe Lane22:02:27

( I REALLY appreciate your time, wasn’t suggesting to read all those docs)

Joe Lane22:02:46

I already have, several times over, 3/10 wouldn’t recommend

tony.kay22:02:19

so, it is fine to make normal js calls. So, this what, sends an SMS with a code, then you have to tell the lib the code

tony.kay22:02:22

stuff like that?

Joe Lane22:02:46

yup, but organizationally, is that a remote?

Joe Lane22:02:07

I tried modeling a solution after that blog post on remotes as an abstraction

tony.kay22:02:14

man that lib is a model of modern OOP, eh?

Joe Lane22:02:38

Did I mention it mutates Local storage on my behalf haha

Joe Lane22:02:02

Also, callbacks everywhere, there are like 9 different js lib versions of this cognito thing too.

tony.kay22:02:06

yeah, so using remotes will get the async mess out of your face. Alternatively, you could use all mutations. It is legal to call ptransact! in mutations (or even callbacks within mutations)…it just puts transact! in a setTimeout

Joe Lane22:02:26

The big concern is, 1. should a self contained component of functionality like this be a remote? 2. When I do s3 file uploading, it wont be a multipart upload to my server, instead I’ll be using the js s3 sdk, does that happen in a remote as well?

tony.kay22:02:30

then just swap it all into state and use proper follow-on reads to get UI refresh

tony.kay23:02:51

So, a remote in Fulcro is very simple: It essentially is a send function that is called with a request (the tx, essentially) and some functions like ok, error, and optionally update. When you call ok, it is essentially just Fulcro merge…that is to say it uses the tx and body you send it to put data in app state.

tony.kay23:02:14

so, any async API could be “wrapped” in a remote, and result in any kind of app state change.

tony.kay23:02:28

does not have to have anything to do with networking, even.

tony.kay23:02:00

e.g. you could say “merge [:x] Using body {:x 1}” and you’ll get :x 1 in the root of your app db

tony.kay23:02:14

if you use a true component query, then it’ll do normalization and everything

tony.kay23:02:42

update accepts the same thing, but can be called more than once, but your remote has to indicate it supports incremental update

tony.kay23:02:57

you MUST call ok or error exactly once.

tony.kay23:02:07

that’s really all.

tony.kay23:02:48

That blog article is using the old network protocol

tony.kay23:02:55

(which should still work)

tony.kay23:02:17

but ignore my earlier link…you don’t want middleware cause you’re not doing networking

tony.kay23:02:48

FulcroRemoteI is the newer protocol

Joe Lane23:02:07

Below is my :auth remote

(defn ^:export init []
  (reset! SPA (fc/new-fulcro-client
                :started-callback (fn [app]
                                    ;(initial-load app)
                                    )
                ;; This ensures your client can talk to a CSRF-protected server.
                ;; See middleware.clj to see how the token is embedded into the HTML
                :networking {:auth   (cognito/cognito-auth
                                       {:user-pool-id "us-east-1_somePool"
                                        :client-id    "SomeClientId"
                                        :region       "us-east-1"})
                             :remote (net/fulcro-http-remote
                                       {:url "/api"})}))
  (start))

In the cognito ns
defn cognito-auth
  [{:keys [user-pool-id client-id region]}]
  (let [user-pool (user-pool user-pool-id client-id)]
    (map->CognitoAuth {:current-state (atom {::user-pool user-pool
                                             ::user-pool-id user-pool-id
                                             ::client-id client-id})
                       :parser        (prim/parser {:read cognito-read :mutate cognito-mutate})}))
And i have a record called CognitoAuth, what protocol does this need to implement. Ahh you just answered it.

Joe Lane23:02:34

I was getting a browser exception saying Invalid network

Joe Lane23:02:56

and I believe its due to not implementing the FulcroRemoteI protocol.

tony.kay23:02:20

(transmit [this {::fulcro.client.network/keys [:edn :ok-handler :error-handler :progress-handler] :as raw-request}]

Joe Lane23:02:24

Thank you so much, I think I see it now.

tony.kay23:02:39

doc strings in protocol should help

Joe Lane23:02:40

I was terrified I was going down the wrong path with remotes.

tony.kay23:02:35

sorry, the docs could be better on that

Joe Lane23:02:01

I think the comment above the FulcroRemoteI was confusing me earlier on. It reads, ; Newer protocol that should be used for new networking remotes., which made me think since I wasn’t making a networking remote I was going in the wrong direction. Thought I didn’t need to implement that protocol, then hit this wall.

tony.kay23:02:33

sry for the frustration

Joe Lane23:02:05

No worries, I really appreciate your help. Thank you.

tony.kay23:02:41

@joe.lane tip: If you call ok handler with an ident-join query, then you can target a table entry, if that helps.

{:transaction [{[:user/id 3] [:user/name]}]
 :body {[:user/id 3] {:user/name "Bob"}}}

tony.kay23:02:44

that kind of thing

tony.kay23:02:17

the biggest “sticking point” will be when you want to use a component query, since that will likely create dep loops.

tony.kay23:02:50

in that case, you should probably pass the UI classes as args to the network on creation

tony.kay23:02:54

or something of the sort

tony.kay23:02:18

{:transaction [{:top-key (prim/get-query User)}]
 :body {:top-key {:user/id 1 :user/name "Joe"}}}

Joe Lane23:02:21

Oh interesting. I’m still working on wrapping my head entirely around the basics of the framework but I’m sure this will come in handy sooner than I think.

tony.kay23:02:59

So, Fulcro treats all data on equal footing: incoming data is a tree. Tree is merged using a query from components (that have idents) which is how normalization works.

tony.kay23:02:19

initial db state is just this applied with root query to initial tree

tony.kay23:02:03

mutations can “bit twiddle” things in, but in general you want to deal with graphs of data (usu trees on input) where the component-based (sub) query is responsible for organizing it into you db

tony.kay23:02:31

From there, refresh is based on you saying “these props changed” (a follow-on read in a top-level mutation). Fuclro keeps an index of props on-screen, so it can find the things to refresh from there

tony.kay23:02:20

that is the pattern…pretty much everywhere…it’s easy to get lost in the weeds, but it really is quite simple

Joe Lane23:02:46

I’m going to read through the above a few times, but I found this https://github.com/levitanong/fulcro-rest-remote/blob/master/src/main/fulcro-rest-remote/core.cljc and think it will be a decent reference for other remotes going forward. I’m probably also going to go back through some of your older youtube videos like this one https://www.youtube.com/watch?v=mT4jJHf929Q&amp;t=2s

tony.kay23:02:22

yep. The concepts in that white board video are still pretty much spot on. The references to Om Next internals no longer matter, and I say the wrong library name a lot, but other than that it is still correct. There are additional features at this point, but all of that is still really useful to know.