Fork me on GitHub
#fulcro
<
2019-02-03
>
hmaurer00:02:46

How do you go about debugging an error list this? https://puu.sh/CGnmY/72db0997df.png

hmaurer00:02:25

looking at the function in parser.cljc it seems like it failed an expression to an ast, which I assumes is something fulcro calls on queries?

hmaurer00:02:04

but I am only getting this error on a hard load (refreshing the page), and it doesn’t render properly. If I make an edit it triggers a hot reload and the error doesn’t appear (it runs the query fine and displays the data)

hmaurer00:02:50

I found the source of the issue, but more by chance than by reading the error message. If anyone has debugging advice to share in this sort of scenario, please do 🙂

tony.kay03:02:44

definitely get your devtools installed and working so you can read them better. Errors in the parser are tough, since it generally means either the query or the mutation you submitted either didn’t parse, or that the handler for the mutation threw an exception…you can be defensive and wrap your mutation action bodies in try/catch

hmaurer13:02:51

Thanks! In my case the issue seemed to be coming from a malformed ident (one-element vector instead of two elements). This ident wasn’t sent as part of a query, but still triggered the error. Anyway, fixed!

hmaurer13:02:09

Question: when setting the ident of a screen, why does :ident (fn [] (task-screen-path)) work but :ident task-screen-path does not?

hmaurer13:02:09

Question: when setting the ident of a screen, why does :ident (fn [] (task-screen-path)) work but :ident task-screen-path does not?

tony.kay17:02:39

one returns a vector, and the other is a function (value)….basic Clojure

tony.kay17:02:58

functions are both data and things you can call

tony.kay17:02:30

I see your confusion…the (fn ...) part of defsc is NOT really a lambda…it’s syntax of defsc

hmaurer17:02:57

@tony.kay AH. that explains it! I didnt; understand why I had to wrap it in a lambda that… did nothing

tony.kay17:02:26

should have given you a syntax error on compile, since the non-lambda “look” is required to be a vector

tony.kay17:02:34

I’m guessing it did?

hmaurer17:02:50

yep it did; I was just wondering why, but if ito’s “not really a lambda” that explains it

hmaurer17:02:05

that’s a bit confusing though

tony.kay17:02:35

you’re right…I could easily let the macro allow it, but was trying to make it super regular for better error messages when you do something wrong

tony.kay17:02:29

the number of things that have to “align” is one of the more difficult annoyances of declaring components…the error checking of the non-lambdas is quite useful, and was my main goal

hmaurer17:02:53

@tony.kay imho that’s fair, better error messages are preferable, but in this specific scenario the error message was definitely not understandable by a beginner like me. Perhaps if the macro gave something like the explanation you just gave me as an error in this case it would be better

hmaurer17:02:38

Anyway I totally get that it’s not a priority

hmaurer17:02:16

The error message I got is: > [ 0.785s] [fulcro.client.primitives] malformed Ident. An ident must be a vector of two elements (a keyword and an EDN value). Check the Ident implementation of component elogic-beyond.ui.root/Router-Union .

hmaurer17:02:38

It correctly hinted at where the error was, but didn’t really say why

hmaurer17:02:58

oh and a warning: > [ 0.781s] [fulcro.client.primitives] get-ident returned an invalid ident for class: [object Object]

adamfeldman15:02:30

A Hacker News story made me think of Fulcro, specifically how Fulcro nicely addresses managing state across subsections of the UI tree: "React as a UI Runtime" https://news.ycombinator.com/item?id=19067302, and in particular this comment thread on state machines https://news.ycombinator.com/item?id=19068939

adamfeldman15:02:32

I've taken the liberty of creating a Fulcro stream on the Clojurians Zulip -- https://clojurians.zulipchat.com, https://clojureverse.org/t/introducing-clojurians-zulip/3173

hmaurer18:02:45

@tony.kay Again on routing… I am having the following issue: on initial page load, the default route of my router briefly flashes before transitioning to the current route. This is (I assume) because there is a small time lapse during which the router is on its initial state and hasn’t yet been transitioned to the current route in response to the history event.

hmaurer18:02:44

My first idea to tackle this was to initialise the router’s current-route under :initial-state when building the Fulcro client (which I am trying out right now), but do you have another suggestion?

hmaurer18:02:51

Basically I want to do a set-route* before the initial render. I tried this but it lead to an error:

(fc/new-fulcro-client
  :initial-state (r/set-route* {} :root-router (get-current-screen-path))
...

hmaurer18:02:33

To break it down: Current behaviour: - On page load, application renders router’s default screen. - After a brief moment, the history-listener machinery triggers a mutation which in turns triggers set-route*, switching to the “current screen” (based on URL). Desired behaviour: - On page load, application renders router’s current screen (based on URL).

hmaurer20:02:34

@tony.kay if you get the time to check out this question today / at some point, I haven’t found a solution to it

tony.kay20:02:56

Main approach: Initial state starts with a “ready?” flag set to fasle…when not ready, show a “Loading…” screen. Application looks at URL, sets routes, THEN sets ready true.

tony.kay20:02:12

Alternate: You do an external initialization of state before you mount the app, making sure the state is correct at start

hmaurer20:02:40

@tony.kay I see, ok. So no router state manual initialisation; just don’t render the app until we have set the routes

hmaurer20:02:13

I’ll try both of those approaches. I tried the second one (initialise the state) but got into a “bad route” error. I probably messed it up, will try again

hmaurer20:02:47

@tony.kay I did it like this:

(fc/new-fulcro-client
  :initial-state (r/set-route* {} :root-router (get-current-screen-path))
but that didn’t work (I was using r/set-route* as a helper to initialise the state properly for the route)

tony.kay20:02:23

nope…initial state has to be a tree…that makes normalized state db

hmaurer21:02:51

@tony.kay is the initial state provided there deep-merged with the initial state coming from components? or only shallow-merged?

hmaurer21:02:01

@tony.kay I tried to initialise the state with a tree, by hand, like this:

:initial-state
               {:fulcro.client.routing.routers/by-id
                {:root-router
                 {:fulcro.client.routing/current-route (get-current-route)
                  :fulcro.client.routing/id :root-router}}}
(in new fulcro client) but it messed things up somehow

hmaurer21:02:12

(I do realise “messed things up” isn’t very descriptive)

tony.kay21:02:37

state tree has to match query tree

tony.kay21:02:44

nothing more to it

tony.kay21:02:04

get-current-route????

tony.kay21:02:22

that should be returning a TREE of the state of that route

hmaurer21:02:17

@tony.kay that just returns the ident

hmaurer21:02:35

e.g. [:SCREEN/home :singleton]

hmaurer21:02:54

:initial-state
               {:fulcro.client.routing.routers/by-id
                {:root-router
                 {:fulcro.client.routing/current-route [:SCREEN/home :singleton]
                  :fulcro.client.routing/id :root-router}}}

tony.kay21:02:09

that is not a TREE

tony.kay21:02:12

that is an ident

hmaurer21:02:18

Indeed it is

tony.kay21:02:21

initial state MUST be the TREE (non-normalized)

tony.kay21:02:42

unless you supply an atom (then you can supply a competely normalized db)

tony.kay21:02:11

look at what a normal call to get-initial-state would return from root: it is a nested tree data structure, NOT a normalized db

tony.kay21:02:17

Fulcro normalizes it at start

tony.kay21:02:27

that makes it convenient for composition of UI in code

hmaurer21:02:51

@tony.kay how does it merge the state returned by get-initial-state on the root and the state given by :initial-state on the Fulcro client?

hmaurer21:02:59

Does it do a deep merge of the two trees before normalising?

tony.kay21:02:34

IF you supply :initial-state it does NOT use root…well-documented

hmaurer21:02:58

AH. ok. Sorry, I haven’t read the book from end to end; just sections. My bad

hmaurer21:02:39

so I guess I should call get-initial-state myself on the root, then maybe set my current route on that tree using an assoc-in, and set that as initial state

hmaurer21:02:40

that might work

tony.kay21:02:42

Um…probably more on the doc string of the client createion

tony.kay21:02:47

Here’s what I’d do: 1. Call get-intiitla state on root 2. Use tree->db to normalize it (using get-query on root for the query) and put that in an atom. 3. Use mutation helpers and swap on the resulting thing 4. Pass the final atom to the new client

tony.kay21:02:05

basically, you’re then manually doing what Fulcro does automatically

tony.kay21:02:22

That’s why I like the flag method better…just mark it as not ready, and queue a sequence of mutations

hmaurer21:02:23

yep, that makes sense. Thanks!

hmaurer21:02:15

That allows me to also do it on the server-side though. I could even populate in route data

hmaurer21:02:29

(not something I am going to do right now, but seems like it woudl work within that approach)

hmaurer21:02:35

Anyway, I’ve used enough of your time for today. Thanks as always 🙂

hmaurer18:02:57

(I know you are not very familiar with HTML5 routing but I think it’s irrelevant here)

hmaurer18:02:58

From what I can see @claudiu you are dealing with this in the router’s component initial state: https://github.com/claudiu-apetrei/fulcro-nav-router/blob/develop/src/main/fulcro_nav_router/core.cljc#L55 :thinking_face:

hmaurer18:02:49

@claudiu I don’t fully understand your code though. Do you get the flickering effect I am having? And if so, what would be your approach to fix it?

claudiu18:02:50

The nav-router code with build-initial-state is because of dynamic-queries (for code splitting)

hmaurer18:02:50

@claudiu but that triggers after the first render, doesn’t it? :thinking_face:

claudiu19:02:02

yep. Can't remember a flicker, but was not really paying attention.

hmaurer20:02:14

@tony.kay bit of an implementation-specific question: I need to trigger a transaction within a component then run a function AFTER the UI has re-rendered as a result of the transaction. Experimenting I found that this worked:

:onChange (fn [evt]
  (m/set-string! this :ui/new-formula-value :event evt)
  (js/setTimeout (fn [] (log/info "AFTER TIMEOUT"))
                            0))

hmaurer20:02:20

am I guaranteed this will always work?

hmaurer20:02:11

😕 is there another way to achieve what I want to achieve?

hmaurer20:02:22

in a previous app I used the callback on react’s setState

hmaurer20:02:31

but here I am not using component-local state; I am using Fulcro’s global state

tony.kay20:02:41

componentDidUpdate

tony.kay20:02:01

but read the SO answers for more ideas

hmaurer20:02:07

so I would need to store the function I want to run on the component, and run it on componentDidUpdate?

hmaurer20:02:10

i.e. “queue it up”

hmaurer20:02:14

and purge the queue on componentDidUpdate

tony.kay20:02:30

depends on what you’re trying to do…pessimistic mutations cover most of the use-cases

tony.kay20:02:51

but if you truly need to do something “after render”, then that’s limited by what React provides

hmaurer20:02:37

Simple: I want to write an input component which allows you to enter special characters. It looks like this: https://puu.sh/CGL75/a4543e726b.png. When you click on one of those buttons it will add the character to the input’s current value, but I need to re-position the input carret (using setSelectionRange) after render, otherwise it won’t be where the user expects it to be

tony.kay20:02:17

this is probably a case where you use react component-local state 🙂

tony.kay20:02:35

because it is the only kind guaranteed to behave correctly at that close to the DOM

hmaurer20:02:55

Hmm but then my input would be uncontrolled, which isn’t as nice to use from the parent, it seems

tony.kay20:02:57

Fulcro can’t magically make React do things it can’t do 🙂

hmaurer20:02:03

True that. Thank you 🙂

tony.kay20:02:34

so, you can still make the component “appear controlled”, which is in fact what Fulcro does to all react inputs…but it’s a messy hack

hmaurer20:02:05

right, I would need to add a componentWillReceiveProps lifecycle hook, and do messy things there, no?

hmaurer20:02:22

updating the local state to keep it in sync

tony.kay20:02:26

The internal hack can be see in the wrapper code for DOM in Fulcro

tony.kay20:02:35

w.r.p. is deprecated

hmaurer20:02:40

why do you do this with react inputs by the way?

hmaurer20:02:44

instead of fully controlled?

tony.kay20:02:44

but yes, that’s essentially it

tony.kay20:02:59

“fully controlled” isn’t possible in React unless you use component-local state

tony.kay20:02:25

so, in systems like Fulcro that want the control to come from another source of truth, you have to kind of hack it in

hmaurer20:02:36

Why? Can’t you have an input’s value set from the result of a query, and spit a mutation in response to onChange?

hmaurer20:02:44

(that’s what I seem to be doing right now :thinking_face: )

hmaurer20:02:46

I mean, I am using the wrapper code I guess, but react inputs are controlled by default if you pass a non-null value to :value?

tony.kay20:02:47

show me an example in their docs where “controlled” isn’t component-local state 🙂

hmaurer21:02:09

ah interesting, thanks!

hmaurer20:02:38

@tony.kay there isn’t an example in their doc but unless I am terribly mistaken it works fine with global state; I have done it before. The component doesn’t care, really…

hmaurer20:02:54

In fact I am sure of it, librairies like Formik use global state by default

hmaurer21:02:00

you just get latency issues in some cases…

hmaurer21:02:05

so component-local state is preferred since latency is very noticeable when you are typing in an input

tony.kay21:02:10

If you try to control an input from an async update, then you;ll get cursor jump to end problems

tony.kay21:02:17

AND latency

tony.kay21:02:04

See the stack overflow thing I just referred

hmaurer21:02:28

Yeah, but I am getting that “cursor jump to end” problem either way with my input because of the extra buttons to add special characters. Anyway I don’t want to waste your time on this, you gave me enough pointers; I think I know how to fix it now 🙂

hmaurer21:02:54

This is the code I had before, on my vanilla React version:

insertUnicodeSymbol = (sym: string) =>
    () => {
      const { value } = this.state;
      const selectionStart = this.refs.input.selectionStart;
      const selectionEnd = this.refs.input.selectionEnd;

      const newValue = value.substring(0, selectionStart) +
                       sym +
                       value.substring(selectionEnd, value.length);

      this.setState({
        value: newValue,
      }, () => {
        this.refs.input.focus();
        this.refs.input.setSelectionRange(
          selectionStart + sym.length, selectionStart + sym.length
        );
        this.props.onChange(newValue);
      });
    }

hmaurer21:02:08

(you can see the code to deal with the cursor jump to end problem there)

tony.kay21:02:08

Yeah, this is just a painful aspect of DOM sync with React…just have to understand the React aspects

tony.kay21:02:30

and Fulcro’s set state supports giving the callback

hmaurer21:02:56

Yep, but i was trying to keep the value in global state. I guess I’ll use give up on that and use component-local state

hmaurer21:02:25

I guess it’s just a place where you need to use a slightly impure model

tony.kay21:02:35

Unfortunately that is basically what you have to do because the React Magic is only supported via sync calls to setState; HOWEVER, there is nothing that prevents you from making that look controlled from outside the compontn

tony.kay21:02:58

which is what we’re doing with the DOM inputs in fulcro.client.dom

hmaurer21:02:15

I’ll look at your DOM inputs to see how it’s implemented then

tony.kay21:02:36

The trick is essentially to optimistically do the setState so that when the async value comes in from the “model” is already matches the value on the component

tony.kay21:02:16

so that “external users” of your component can pass you an async value

hmaurer21:02:23

and if it doesn’t match? you override the state?

tony.kay21:02:44

BTW, you can leverage that function to add wrapping to you own (or js ecosystem) components…I do that for the react semantic UI wrappers

hmaurer21:02:56

Ah neat! Can I use this even if my “Input” component doesn’t have an input at the top-level? Surely it must expose onChange and other props?

tony.kay21:02:11

Of course that wrapper assumes the “element” looks like a regular DOM input

hmaurer21:02:16

@tony.kay ah yeah, quite a bit of fluff there. By the way I saw you using gobj/set etc in the book. Any reason to use it over (set! ...)?

hmaurer21:02:29

when setting a property with a known name

tony.kay21:02:01

setting by string is a safer thing to do in this case over adv optimization (prevents symbol rename)

tony.kay21:02:10

but I admit I’m not always “sanely consistent” with the usage 😜

tony.kay21:02:49

much of the code in those areas was “inherited” from other places (e.g. Om Next), where I did not necessarily write it or have knowledge of the original authors intent myself.

Lu22:02:48

By the way I just put together a light form library that is very customizable and does basically everything for you 🙂.. you can check it out here

hmaurer23:02:18

Why am I getting this warning and yet I can access this? https://puu.sh/CGQKz/ddd1440c85.png

pvillegas1201:02:15

defsc macro gives you access to this but not props (second argument)

pvillegas1201:02:33

Inside those hooks like :initLocalState

hmaurer01:02:22

@U6Y72LQ4A ah; that’s odd. Is there any other way I can access the props?

pvillegas1201:02:04

yes, look at the primitives namespace, there should be a get-props or props function given a component instance

hmaurer01:02:14

@U6Y72LQ4A ah yep, I Ctrl-F’d it earlier for get-props, but the function is named props. Thanks!

👍 1
hmaurer01:02:43

@U6Y72LQ4A is there a technical reason for why the macro doesn’t make the props available?

pvillegas1201:02:01

don’t know 🙂

hmaurer01:02:17

@tony.kay is there a technical reason for why the macro doesn’t make the props available? 🙂

tony.kay17:02:49

The signatures of those are aligned to React

tony.kay17:02:45

React does not pass in props, so props are not there…same with all of the lifecycle methods (documented in docstring)

tony.kay17:02:02

You only get what React would have passed in/made directly availble.

tony.kay17:02:54

If there are cases where we “can”, I guess we could…it is an inconsistency in the macro from a “global reader” perspective that cannot be helped in some cases (e.g. get-query must be callable statically).

hmaurer18:02:21

@tony.kay ah; because in the book you mention: > There is no formal constructor for defsc, but you can get exactly the effect of a constructor by placing the code for it in :initLocalState, which is guaranteed to run once and only once per instance construction and react does pass props to class constructors. I believe back when React.createClass was the norm people used getInitialState, which indeed doesn’t have the props as an argument, but when using ES6 classes the state is usually initialised in the constructor, which does have the props.

tony.kay19:02:22

yeah, there is some legacy React 15isms here

tony.kay19:02:26

and it is still supported

tony.kay19:02:12

to date there has been no real outcry for refining lifecycle methods…PRs welcome (assuming they don’t break existing apps)

tony.kay19:02:27

I think you’ll find the a lot of the answers to “why” questions are not terribly satisfying 😉

hmaurer19:02:21

@tony.kay haha. Well, I think if I want to contribute to documentation and/or code, understanding the “why” is often quite important, even if the answer is unsatisfying

hmaurer19:02:49

If you don’t mind me asking those questions of course

tony.kay19:02:48

I don’t mind.

tony.kay19:02:55

so slow on the responses

tony.kay19:02:40

Putting the destructured arguments into the lifecycle methods where possible is one place where we could make the macro better. Supporting symbols for lambdas is another.

tony.kay19:02:46

but i was also concerned with other kinds of potential confusion: some lifecycles give “current” props as args, others give prev props…lots of confusion could ensure from doing anything automatic, which is part of the problem that I was “punting” on

tony.kay19:02:52

also, there is overhead in adding the “wrappers” that provide the functionality (as-in runtime overhead), which is a concern to balance as well

hmaurer19:02:16

@tony.kay ah yes, that’s a good point. :thinking_face: