Fork me on GitHub
#fulcro
<
2019-05-27
>
Chris O’Donnell00:05:57

Huh, I don't see a rule about needing to use const anymore. Could've sworn I remembered that being one of the rules, but I guess I'm mistaken. I'll keep hunting for the rule I'm breaking, then. 🙂

Chris O’Donnell01:05:30

In case it helps anyone, I figured it out. I had defined my hook function like so: (def use-styles (styles/makeStyles (clj->js {:root {:width 500}}))). When I tried to bind (use-styles) to classes in a let binding, that compiled into:

var classes = (little_gift_list.ui.root.use_styles.cljs$core$IFn$_invoke$arity$0 ? little_gift_list.ui.root.use_styles.cljs$core$IFn$_invoke$arity$0() : little_gift_list.ui.root.use_styles.call(null));
I'm guessing that because I didn't define use-styles with defn, the compiler wasn't sure it was a function, and so it added a conditional check for that. The conditional check breaks the rules of hooks. Fortunately, refactoring this to define use-styles with defn fixes it:
var classes = little_gift_list.ui.root.use_styles();

👍 4
hmaurer09:05:42

@codonnell interesting; what error were you getting with the conditional check?

hmaurer11:05:41

to elaborate a bit so you get the full context of my question when you pop online: I don’t see why this conditional check would have given you an error. The “rules” of hooks are not strictly enforced rules, they exist because if you wrote your own in ways that infringes them (e.g. wrapped a hook in a conditional), things would break at runtime. The conditional you have here will always evaluate to the same thing at runtime, unless you were to rebind the var to a non-function (which you won’t do). React should be unable to tell that there is a conditional there, and things should just work. You shouldn’t even care there is a conditional there.

hmaurer12:05:12

The same thing with const. You shouldn’t care about this.

Chris O’Donnell13:05:55

@hmaurer

Uncaught Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See  for tips about how to debug and fix this problem.
And the only difference between getting the error and not was def vs. defn.

hmaurer13:05:16

:thinking_face: odd

hmaurer13:05:38

what’s your code with defn @codonnell?

hmaurer13:05:45

also where is makeStyles coming from? material ui?

Chris O’Donnell13:05:07

Yeah, material ui

Chris O’Donnell13:05:26

(defn use-styles []
  (styles/makeStyles (clj->js {:root {:width 500}})))

eoliphant13:05:50

Yeah I got the exact same message trying to use their hook API with def

hmaurer13:05:14

this defn does something different than def though

hmaurer13:05:24

if you call it in the same way

hmaurer13:05:26

:thinking_face:

Chris O’Donnell13:05:33

Ah, you're right.

hmaurer13:05:49

but still, looking at the material-ui JS code it seems to match what you did with def

Chris O’Donnell13:05:25

No, I think the def was wrong; makeStyles takes a function as its argument that returns a map.

hmaurer13:05:04

how so? from the JS example it seems to take a JS object

hmaurer13:05:24

hang on a sec

hmaurer13:05:45

with the defn version, are you sure it works? As in, not just that it doesn’t throw an error, but are you sure it changes the styles?

Chris O’Donnell13:05:50

No, stopped working yesterday once I got it to compile. Take a look at a component example: https://material-ui.com/components/buttons/. It looks to me like it takes a function rather than plain data.

hmaurer13:05:42

@codonnell I am pretty sure it accepts both. Either a straight up map, or a function taking the theme as argument and returning a map

hmaurer13:05:59

you are free to chose whether you want to parametarise the style on the theme

hmaurer13:05:12

wait a sec

hmaurer13:05:25

@codonnell can you copy/paste (or gist if it’s long) the code you tried to run?

hmaurer13:05:37

Specifically the component in which you tried to call use-styles

hmaurer13:05:33

or more directly, answer this question: did you try to call use-styles within a component defined with defsc?

Chris O’Donnell13:05:03

One sec, converting this into a more manageable example (and closing some tabs so my old laptop stops complaining so much).

Chris O’Donnell13:05:21

It is in a function component rather than inside a defsc

Chris O’Donnell13:05:45

(def use-styles #(styles/makeStyles (clj->js {:root {:width 500}})))

(defn my-nav []
  (let [classes (use-styles)]
    (mui/bottom-navigation {:value 0
                            :onChange (fn [event new-value]
                                        (js/console.warn {:event event
                                                          :new-value new-value}))
                            :showLabels true}
                           (mui/bottom-navigation-action {:label "Recents"
                                                          :icon (icon/restore)})
                           (mui/bottom-navigation-action {:label "Favorites"
                                                          :icon (icon/favorite)})
                           (mui/bottom-navigation-action {:label "Nearby"
                                                          :icon (icon/location-on)}))))

Chris O’Donnell13:05:48

The mui/* and icon/* are just wrappers around (apply react/createElement ElementName (clj->js props) children) (or (react/createElement ElementName) for the 0-arity case).

hmaurer13:05:51

@codonnell how did you include my-nav in the rest of your app?

Chris O’Donnell13:05:29

(defsc Root
  [this {:keys [router header]}]
  {}
  (my-nav))

Chris O’Donnell13:05:11

^ The root component, if it wasn't clear.

Chris O’Donnell13:05:35

The snippet above that I pasted works properly.

hmaurer13:05:37

@codonnell yeah ok, there lies the issue. You aren’t using my-nav as a “function component”

hmaurer13:05:40

you are just calling it as a function

hmaurer13:05:52

from React’s perspective, there is only one component: the Root

hmaurer13:05:55

and that’s a class component

hmaurer13:05:59

in which you can’t call hooks

hmaurer13:05:10

your def version of the hook was correct, the defn was not

hmaurer13:05:28

the defn didn’t throw because it wasn’t calling the hook, but I would bet it wasn’t changing the styles either

hmaurer13:05:01

try creating a factory for your my-nav element like so:

Chris O’Donnell13:05:20

Ah, I need to wrap it with react/createElement don't I

hmaurer13:05:31

(defn ui-my-nav [props]
  (dom/create-element my-nav props))

Chris O’Donnell13:05:14

There it is. 🙂

hmaurer13:05:17

@eoliphant check out the above thread if you didn’t find a solution to the issue when you encountered it

hmaurer13:05:49

TL;DR; don’t try to use hooks in class components. All components defined with defsc are class components.

eoliphant13:05:44

Yeah that was my takeaway @hmaurer, that it wasn’t going to work with defsc’s as they’re ultimately React classes

Chris O’Donnell13:05:59

And call your function components with dom/create-element instead of directly as functions as you would in reagent.

Chris O’Donnell13:05:13

^ This was the real sticking point for me.

hmaurer13:05:08

well with reagent you don’t quite call them directly @codonnell; you use a vector syntax, e.g. [my-app & props]

hmaurer13:05:19

so reagent is free to call create-element when interpreting that vector

Chris O’Donnell13:05:39

Sure. 🙂 It makes the call to create-element less obvious, I suppose.

hmaurer13:05:21

yup. Also note that you could do this in vanilla js as well: just call functions that return JSX to get your app. But component boundaries help react with optimisations, etc

eoliphant13:05:55

so in short, direct use in defsc’s is a no-op, but (dom/create-element func-component props) is the work around? i’m wondering how well that will ‘scale’ in terms of making the most of fulcro, etc

hmaurer14:05:16

in theory we could move to a defsc that uses hooks instead of defining a class component

hmaurer14:05:26

@tony.kay mentioned this before

hmaurer14:05:40

I am not sure how high it is on his todo-list though

hmaurer14:05:35

in the meantime, I guess a work-around would be to wrap your function component into a class component defined with defsc for the sole purpose of running a query and providing the data as props to the function component

hmaurer14:05:01

then you can use hooks as you please in the function component

4
rickmoynihan14:05:19

this looks like an issue I’m trying to find a solution for

rickmoynihan14:05:28

I’m not using fulcro (yet); but I’m looking for a way to hydrate a reactified component from a clj(c) rendered component… from a clj server backend

rickmoynihan14:05:06

and running into these kind of issues… i.e. having a reactified flavour of hiccup that works in clj

tony.kay15:05:14

@rickmoynihan Fulcro’s dom stuff is isomophic to clj/cljs…you don’t need a hiccup thingy…just use the functions. It’s an idential amount of typing.

tony.kay15:05:25

see the book

tony.kay15:05:23

I’m shooting for F3 to let you actually run the app in clj (full-stack with a “loopback remote”)…no real DOM, of course, but running mutations and loads to get it into the right state to spit out the right thing is on my list.

tony.kay15:05:17

@codonnell Hooks are really easy to get working, but you cannot use defsc (because that generates a class with lifecycles)…you have to understand the internals a bit. They’ll be a lot easier to use from F3 as well.

rickmoynihan15:05:54

Sounds interesting… what do you mean by “loopback remote”?

hmaurer15:05:34

a remote that doesn’t really do anything remote; it just hits the server’s API directly internally

hmaurer15:05:52

instead of going through the standard HTTP gateway

hmaurer15:05:02

e.g. it could directly call the router

rickmoynihan15:05:15

ok — so the kind of shimming I’m talking about

hmaurer15:05:20

I am putting words into @tony.kay’s mouth though; hope I’m right 😅

tony.kay15:05:09

It could either be literally a loopback interface to your real API (via 127.0.0.1)…kind of wasteful but simulates everything… or more likely a “remote” that just had a handle to the server-side parser that processes the queries.

rickmoynihan15:05:09

but it’s still running in the same JVM process? i.e. no need for node on the server?

hmaurer15:05:38

JVM everything

👍 4
tony.kay15:05:30

@codonnell Here is an example I did while playing with them for F3…none of this is part of the real code base, but this was working in my playground:

(defn use-fulcro
    "React Hook to simulate hooking fulcro state database up to a hook-based react component."
    [component query ident-fn initial-ident]
    (let [[props setProps] (use-state {})]                  ; this is how the component gets props, and how Fulcro would update them
      (use-effect                                           ; the empty array makes this a didMount effect
        (fn []
          (set! (.-fulcro_query component) query)           ;; record the query and ident function on the component function itself
          (set! (.-fulcro_ident component) ident-fn)
          ;; pull initial props from the database, and set them on the props
          (let [initial-props (prim/db->tree query (get-in @app-db initial-ident) @app-db)]
            (setProps initial-props))
          ;; Add the setProps function to the index so we can call it later (set of functions stored by ident)
          (index! initial-ident setProps)
          ;; cleanup function: drops the update fn from the index
          (fn []
            (drop! initial-ident setProps)))
        #js [])
      props)))
NOTE: THIS DOES NOT WORK IN F2 OR F3…it worked in my playground env for experimenting with the idea of hooks-based Fulcro…but is shows the approximate level of complexity needed to get it working: pretty low. There, of course, is some work to do making the rest of the platform to understand that they are components, but that isn’t too hard.

tony.kay15:05:05

@rickmoynihan Fulcro has had server-side rendering support on JVM for years

tony.kay15:05:11

about 3, I think

tony.kay15:05:04

F2 requires you use the low-level mutations and manipulate the app state into place, then render. The limitation is that you can’t call “load” and such, because the plumbing has some details that don’t work on the JVM…BUT, it works quite well since mutations can easily be CLJC. F3's plumbing is simpler and will work on both easily.

tony.kay15:05:54

I don’t know that I’ll implement lifecycle simulation (e.g. componentDidMount that has loads) on the server, so chances are you’ll still want the old way for most apps: make state, morph it, render

tony.kay15:05:55

though really, just that one lifecycle method probably wouldn’t be that hard to simulate yourself.

rickmoynihan15:05:14

@tony.kay interesting… perhaps you can dissuade me of my ill-formed biases against fulcro 🙂 At first glance it seems like it’s a pretty full stack solution; i.e. requires an all or nothing buy in… IIRC your docs talk about graph databases etc. I’m assuming that database isn’t a “real database” (not to be pejorative); am I right that it’s just a transitory one to manage the flow of data between front and backend? i.e. it’s not feasible for us to change database.

hmaurer15:05:04

@rickmoynihan it depends on your definition of real database; it’s an in-browser datastructure that serves as a cache for the data loaded from the server. It’s completely unrelated to whatever database you might be using on the server

hmaurer15:05:30

it’s not persistent either; it’s an in-memory datastructure

rickmoynihan15:05:49

:thumbsup: ok cool that’s fine it’s not trying to be the single source of truth for my app

hmaurer15:05:56

all Fulcro components pull their data from that datastructure

hmaurer15:05:03

it’s the single source of truth in the browser

hmaurer15:05:12

but you can load data from wherever into it

hmaurer15:05:01

Have you used Relay? (Facebook’s GraphQL client). Or redux?

rickmoynihan15:05:44

Not used relay or redux… but I’m vaguely familiar with the ideas. Have implemented some graphql.

hmaurer15:05:48

The basic idea is that you want a place to put the data you load from the server. Fulcro normalises the data you load as well, which means that if you load information about the same conceptual “entity” from a server (“remote” in Fulcro’s parlance), say from a single “person”, Fulcro will store it together. This helps keep things consistent

hmaurer15:05:06

Fulcro’s “graph database” is really just a big map

hmaurer15:05:36

it has none of the features you would traditionally associate with the word “database”

hmaurer15:05:00

call it a “normalised data cache”

hmaurer15:05:43

I mentioned Relay because they do the exact same, albeit in a more obscure way

hmaurer15:05:09

And Redux doesn’t normalise by default but some librairies let you do so, e.g. https://github.com/paularmstrong/normalizr

rickmoynihan15:05:14

yeah those two can be easy to mix up in casual conversation 🙂

hmaurer15:05:25

> At first glance it seems like it’s a pretty full stack solution; i.e. requires an all or nothing buy in… Somewhat I guess. As far as I know (at least from my use-case), Fulcro doesn’t really appear on the back-end. It encourages you to use Pathom as a layer between the client requests and your business logic (which serves a similar purpose as GraphQL), but you are not obliged to (though you should really try it out, it’s awesome)

tony.kay15:05:30

Here’s the basic central concept that you need to grok about Fulcro: It’s central job is to make it easy for you to deal with the graph of data. There are these core tasks: 1. Getting the data from the server 2. Getting data into the UI tree 3. SHARING data among the elements in the tree. Fulcro does this mostly for you by defining a few very simple concepts/ideas. IDENT: A component tells Fulcro “here’s which table my data lives in, and here’s how to derive the ID of that when you see data from a server. QUERY: A component tells Fulcro “here’s the stuff I need, and I also want to render these children..so you’ll need to have their stuff too”

(defsc Child [this props]
  {:ident (fn [] [:child-table-name (:child/id props)])
   :query [:child/id :child/name]} ...)

(defsc Parent [this props]
  {:ident (fn [] [:parent-table-name (:parent/id props)]
   :query [:parent/id :parent/data {:relation (get-query Child)}]})

hmaurer15:05:47

I was going to keep talking but I’ll let @tony.kay take it from there 😬😄

tony.kay15:05:38

Now, Fulcro can solve all of the problems with just this data: 1. Load. It can form a graph query to get Parent (and child because it was composed in)

tony.kay15:05:04

The server returns something like {:parent/id 1 :relation {:child/id 2 :child-name "A"}}

tony.kay15:05:38

Fulcro can use it’s knowledge of the shape of this to NORMALIZE the child INTO a table…same with parent. I just writes (into app db on browser):

tony.kay15:05:20

{:parent-table-name {1 {:parent/id 1 :relation [:child-table-name 2]}}
 :child-table-name {2 {:child/id 2 :child/name "A"}}}

tony.kay15:05:46

Now, to hydrate the UI, it can do the reverse…again, using the query, it can denormalize back into the original tree

rickmoynihan15:05:11

cool - how does it pass the data to the client for hydration? data-attr, ajax call?

tony.kay15:05:15

Then for SHARING: Since things are normalized, you can have 20 diff components that all say they come form the “child table”, and they automatically share data

tony.kay15:05:27

this is all client

tony.kay15:05:41

the only thing that came from an imagined server was the graph response to the query

tony.kay15:05:48

{:parent/id 1 :relation {:child/id 2 :child-name "A"}}

rickmoynihan15:05:09

ok — but if the component has been rendered SSR you need to send the initial state to hydrate it somehow, so it can render the next state right?

tony.kay15:05:12

The book explains all of this, and I’m quite busy…I’ll let the book and perhaps Henri take it from here 🙂

rickmoynihan15:05:29

😀 thanks for your help

tony.kay15:05:36

SSR is “render the first frame fast”, then let the client take over…you just normalize the client browser db and embed it as data in the initial page

hmaurer15:05:38

@rickmoynihan yeah hydrating is pretty easy, I have done it before

tony.kay15:05:40

See the SSR chapter

hmaurer15:05:06

e.g. you could load some data on the backend, shove it in a JS object and render it as part of the page

hmaurer15:05:19

then Fulcro can pick it up on load and move it into its “graph database” (its cache)

rickmoynihan15:05:46

yes I know 🙂 that’s what I was asking… where is the initial state serialised?

hmaurer15:05:50

I have personally only done that for auth data (a map of info about the current user) because I didn’t want to load that async

hmaurer15:05:54

but you could do it for anything…

rickmoynihan15:05:01

👀 at SSR chapter

hmaurer15:05:07

> where is the initial state serialised? what do you mean?

hmaurer15:05:34

it’s not serialised anywhere; by default there is no initial state (well, not server-supplied; every component can define its own initial state)

hmaurer15:05:45

if you want to server-supply some initial state you can easily do it using Fulcro’s primitives

hmaurer15:05:59

but it’s not going to pick it up for you from some magic location, if that’s what you are wondering

hmaurer15:05:31

you’ll have to write some code that 1/ retrives initial state from some JS variable 2/ shoves it into Fulcro’s database

rickmoynihan16:05:29

ok cool. That’s effectively what I’ve done already with my home-grown reagent SSR solution.

hmaurer16:05:47

it shouldn’t feel too foreign to you then, beyond the initial Fulcro learning curve

rickmoynihan16:05:51

I also agree the server can’t do it automatically… or rather if it does, it’s probably doing too much.

rickmoynihan16:05:12

but it’s good to have the primitives to do it… which is really what I’m looking for.

hmaurer16:05:27

it could, but @tony.kay’s philosophy with Fulcro (as far as I can tell) is to provide primitive operations and not be too opinionated about those things

rickmoynihan16:05:33

I was writing transit json into a data-attr

rickmoynihan16:05:49

thanks I have it open 🙂

hmaurer16:05:00

> I was writing transit json into a data-attr yeah you could do that