Fork me on GitHub
#fulcro
<
2017-08-31
>
denis-v06:08:37

@tony.kay Thanks for your comments and code. But I don't want to use the Company factory when rendering a Person, as a Company will have other fields that I don't want to show in that case. Also, a Person will end up with several more foreign keys, and I just want to display the name field for each one. I've tried including {:person/company (om/get-query Company)} in the Person query, but I still can't get it to work. So it would be good if you could take a look at this gist and point out the obvious mistake: https://gist.github.com/v-sined/0a586f2b1cd03b086e06615a1c9d9351

cjmurphy07:08:23

@denis-v: When you are creating initial state for a person you are using :person/company, yet de-structuring params into just :company. Also initial state is supposed to be tree data so idents should not feature there.

tony.kay07:08:22

@denis-v Yes, what cjmurphy said. Your initial state should be a tree, as should your query. Normalization is done for you. You use a factory to render each component of its own type. A company factory for rendering company, etc. This is part of making a component act as a component, so I’m not sure what you mean there.

tony.kay07:08:37

If you put an ident in InitialAppState you’re doing it wrong. The idents are figured out from your ident functions during initialization. The point of initial app state is that you don’t have to think about the normalized database, you just co-locate and compose. The initial database is built for you.

tony.kay07:08:03

But for everything to work, you have to follow the basic rules: Each component has (optional) initial state that composes in the initial state of any childre. Each component has a query for what it wants, and composes in any children (via joins). The render pulls things out of props and renders a (sub)tree of UI that matches the structure of the initial state and query. It all follows the same shape.

tony.kay07:08:45

The state for a component ends up at its ident in the db, so mutations can be considered with only local (to the component) knowledge…since an update-in at that ident will update any state having to do with what that component renders.

tony.kay07:08:56

With regard to rendering company: my comments in my code were that you call companies react factory FROM person, you don’t render the details of company in Person…that is Company renderer’s job…but you do have to call it from person if you’ve composed it that way, because that’s where the data will be.

tony.kay07:08:59

Have you watched any of the youtube videos? Perhaps seeing some coded live might help. Devcards with inspect-data can also help with insight.

tony.kay07:08:15

I think your use of idents on line 68 is because you think you need to prevent duplicates…but the data will normalize into the same spot…just put a map with company’s data there (in both places). The ident will appear for you.

tony.kay07:08:56

Also: get-initial-state calls initial-state on the given class. It isn’t a needed “wrapper” for a map, as you’ve done on line 29. You destructuring on line 16 won’t pull the data out right (you’ve not namespaced the params, but you sent them in namespaced).

tony.kay07:08:28

If you need a function to just make a company, just make a plain function. Companies and People are typically going to come from a server, and are not meant to be part of initial state…just make a (make-company) function. For demo purposes (since that is what you’re doing), call that function once into a common variable, since you’re wanting to pepper it about in app state (def the-company (make-company 1 "name" ...))

tony.kay07:08:15

Then just drop the-company (which will be a map) anywhere where you need it. It doesn’t matter that you’re “duplicating it”…all of the copies will just overwrite themselves at the same spot and leave the correct idents in app state.

tony.kay07:08:55

Again, this isn’t stuff that would “normally” be in initial app state. So putting them there can be done as a convenience, but since you’re purposely wanting to have pointers the one thing spread out over the app, it’ll be better if you just make the one thing, plop it in the initial state “tree” everywhere you need it, then look at what happens in the database: You’ll end up with one in a table, and an ident everywhere you had plopped a copy.

tony.kay07:08:21

The if you run a mutation on the one in the table (with a follow on read of what the mutation changes) all of the appropriate on-screen components will update…but this also points out why line 57 is wrong no matter what (even if you correct you app state). If you’d done your app state correctly (and fix the bug at line 16 so the data actually ends up in that map, and the bug with putting idents in the initial app state tree), it would still be wrong to render company name in person….person did’nt query for company name…it queried for person attributes and :person/company. Om Next won’t re-render person if a mutation changes company/name: it’ll re-render company. You can have any number of components that reprenset a company (CompanyLineItem, CompanyTableCell, CompanyDetail, CompanySummary) all with the same ident. They’ll all share the same db state, and can all render differently. You have to make one to render company details for person, because them’s the rules 🙂 Components should only render the details the specifically query for. Person queried for a company (in the abstract). It is required to use whichever kind of component is composed the query for. Compose the query for CompanySummary, then you better render with a factory for CompanySummary. The practical reason is that the component indexer is what is used to figure out what to re-render, and if you break into the abstractions your crossing a line the indexer doesn’t now about. Peeking into the data like you’re doing breaks encapsulation, and it breaks the rendering model. The rendering model is easy to reason about because of normalization and component-local concerns. Start stealing queries and peeking into children and you’re making a mess of the simplicity. You cannot reason about programs in the large if you do that. Stick to the abstractions.

denis-v07:08:09

@tony.kay Thank you for your many comments. I will try to digest them and probably come back with more questions. I've tried to copy the pattern in the Automatic Normal section of the Getting Started doc, but obviously I didn't implement it correctly! (The idents in the initial state were a recent change just before I posted the gist.)

tony.kay07:08:16

oh, ok. Yes, you’re doing something fairly advanced for a beginner: most people don’t start with an example where they’re doing something with the same data spread out like this. It is quite simple to do once you understand the concepts. I guess jumping in the deep end will force you to get them more quickly and deeply 😉

tony.kay07:08:47

your bug on 16 is why you’re not seeing data (well, and the idents)

tony.kay07:08:58

everything in that constructed object will be nil

tony.kay07:08:21

because of what you pass on line 29

tony.kay07:08:32

so either fix 29 to not be namespaced kws, or vice versa

tony.kay07:08:53

The “put the same map in many places in the initial state tree” is not intuitive. People think they are duplicating data.

tony.kay07:08:49

but that is essentially the trick: it is duplicated across the tree. It becomes singular due to normalization. But when you render, is becomes duplicated across that render’s tree. Putting state into the db goes through normalization (tree->db will cause all copies of the same thing to merge into one) and rendering with a query is a (db->tree) operation that takes the one and puts it in the render data tree whereever it is needed.

tony.kay07:08:17

initial-app-state is generating the tree as an input to startup…stuff will be duplicated if it needs to render in more than one place, but auto-normalization will fix it.

tony.kay07:08:54

As it would if you ran a UI query against a server (which is a tree query) that returned the same data in two different places in the return value from the server…they could even be different subviews of the same data (CompanyDetail vs CompanySummary). The merge would make sure the props from both would properly merge into the db.

tony.kay08:08:47

This last bit is something that Fulcro is kind of careful about. It takes care to not overwrite things that are not queried for, so that the merge of such a case will work correctly.

tony.kay08:08:06

(stock Om Next would cause one to clobber the other)

denis-v08:08:21

@tony.kay Just to make sure I understand your previous group of comments, if I want to display just a Company's name for a Person, then I need to create a CompanyName component that renders only that field?

tony.kay08:08:59

Or make it so you’re pretending that company name is part of person in your model

tony.kay08:08:14

(if you never needed company to have a larger presence)

tony.kay08:08:11

A join on query means you have to have a component to handle that join. The join needs query/ident for normalization, and the rendering refreshes will be with respect to that component.

tony.kay08:08:59

If you choose to make company name part of person (e.g. :person/company-name) you need only modify the server model to figure out how to put the two together when you query for person.

tony.kay08:08:04

it doesn’t matter how you actually store it on the server…that is the point of the parser on the server…you can morph it however you need to be able to view it.

tony.kay08:08:38

and client-side load post-mutations…change what you got from the server to match how you want to view it

tony.kay08:08:42

a component, having normalization, is nice because updates to that entity will reflect everywhere.

denis-v08:08:34

Thanks again for all your comments. I'm in the early stages of an application that will use Datomic on the backend. The data model will have a lot of relationships. So I want to make sure I'm doing it right on the client side before I get too far down the track and start working with the server.

tony.kay08:08:38

peppering company name into person would mean you’d have to tell mutations that you changed :company/name and :person/company-name…and your mutation would have to do so, which is kind of a pain. Use a component. It’s so much simpler.

claudiu08:08:57

@tony.kay I remember reading something along the lines of "it's acceptable or it's a common pattern to make a component with just ident/query to use for getting the data in another component". But can't seem to remember/find where I got the idea from or if I remember correctly. Is that an "ok approach" for some cases ?

tony.kay08:08:30

@claudiu It is certainly something you do. I think I mention it again in Core Concepts 3 video.

tony.kay08:08:46

Typically for getting data from the server, though

tony.kay08:08:18

you wouldn’t do it in the UI sense to pull data from one to another. A component that pulls data for the UI must render that data.

tony.kay08:08:36

otherwise refresh of UI breaks (as does local reasoning).

claudiu08:08:29

Yep. A use-case I had, was a crazy screen where I needed to a few bits of data from 3 components(entities), that entangled in the layout. I was thinking just create 3 small components to get the data and render it it that component.

tony.kay08:08:28

In that case you might be better off with what I just said: don’t write new components at all: pretend that data lives in the spot you need it…this only works if you don’t ever show that data elsewhere in the UI (can do without normalization). You can use server-side tricks to get the data delivered with the component itself.

tony.kay08:08:49

If you make 3 small components to get bits of data that are also used (and owned) by another component, then you want normalization, and your “little” components should also be “little” renderers so UI refresh can work.

tony.kay08:08:59

The one caveat is if that data does not change during the display of it (refresh isn’t needed)…then you are not going to experience a problem until you later extend the program with server push updates of things, and your UI suddenly doesn’t track everything properly 😉

tony.kay08:08:54

the little components need not share the full query of the sister components…that’s the point of data driven. Each “version” of a view of something asks for just what it needs.

tony.kay08:08:32

which also tunes re-render. If you didn’t change the items it queries, even if you modified the thing (by ident) it queries, it won’t redraw

claudiu08:08:03

Ahh 🙂 Most of my data is already loaded (for listing). I have to display that in a overlay. I was thinking of {:modal/by-type {:overlay {:articles {} :categories {} :user {}}} the 3 components would have no render, just query & ident to get the data from the state. And all the render would be in the overlayComponent.

tony.kay08:08:33

Yeah, but if the data changes, they won’t refresh right.

claudiu08:08:44

Would reference queries be a better fit for this scenario ?

tony.kay08:08:56

No such thing. You mean link queries? The rules for re-render are quite simple: 1. A transact! or set-query! will re-render the subtree of the component on which they are called. shouldComponentUpdate will short-circuit any renders where data didn’t change. 2 A transact! can also have follow-on reads (keywords after the mutations). Every component on screen is indexed by the properties it queried for. So, if the follow-on read is :person/name, then all component on-screen that contain :person/name in their query will refresh. That’s pretty much it. You can use an ident in a follow-on read to refresh any “views” of that db entry on screen.

tony.kay08:08:45

so, no real magic. Rendering is as simple as “say what changed”, and anything that asked for it will re-render.

tony.kay08:08:10

abstract and data-driven. A transacting component need only know the basic idea of what a mutation does (e.g. what properties it might affect)

claudiu08:08:37

I meant query :name [user-by/id 3] and I get a key [user-by/id 3] in props with all the data in the state for that entity.

tony.kay08:08:07

Right. Did you query for :user/name? No. A follow-on read for :user/name will not refresh your view.

tony.kay08:08:21

(assuming :user/by-id has a :user/name you might render)

tony.kay08:08:40

just think through the implication of the rules I just stated

tony.kay08:08:06

it means that a component is always a co-location of query, ident, AND render. And they stick together.

tony.kay08:08:45

The case of using a defui just for a query and ident is for pulling (or merging) data from a server. Not for mixing into the UI.

tony.kay08:08:49

Say your UI is showing people grouped by favorite color. Your graph db on the server won’t have that in the natural join edges…it is a pure invention. You’re probably going to need to pull the people with their fav color. That is your defui stuff without UI. A post-mutation would shape it into a form that your real UI components would use.

tony.kay08:08:08

but those real ui components would do the rendering

tony.kay08:08:07

server tree -> temp ui query -> temp ui state db -> post-mutation -> desired UI state db graph -> render

claudiu08:08:38

Yep. Just the fact that I have a screen that needs data from 3 entities. And for example the person data I have to display in 3 sections of the page. I don't really like having to create 4 different components for bits of person data in that overlay. 🙂

tony.kay08:08:29

Each component has a different UI purpose. You should write 4 IMHO.

tony.kay08:08:24

or maybe you make your other version multi-purpose and pass them a computed prop to change how they render.

tony.kay08:08:42

all other options create non-local reasoning that leads to much much harder problems than just having “a little extra code”

tony.kay08:08:47

other systems might give you “subscriptions” or “signal graphs”…save you a few lines of code, but see how they scale in reasoning as apps get big. The elegance of this approach is that your various views of the same data are clearly defined by the co-located query and ident, which is the same thing that gives you all of the power of data-driven behaviors. Very little to learn. Very little to reason about. Look at the rules I gave you for UI refresh: 2!!!. Try to debug a problem with system that use more complicated ways of updating things.

tony.kay08:08:11

You’re also in a homoiconic language with tractable metaprogramming…you can fix the “I’m typing too much boilerplate” by writing a few choice macros.

claudiu08:08:42

yeps 🙂 So a minimal example of what I was thinking of would be:

tony.kay08:08:14

Yeah, I understood what you meant. Don’t do that 😜

tony.kay08:08:38

If grant’s name changes while the modal is up, it won’t refresh

claudiu08:08:59

With the component approach I would have to write a component for user-name, another for user-age. etc. 😞

tony.kay08:08:22

No, write one component, and pass it parameters via computed to limit how it renders

tony.kay08:08:43

(ui-person (om/computed p {:fields [:name]}))

tony.kay08:08:17

this just doesn’t come up that often that you only want to render on bit here, and one bit there. That’s false accounting

tony.kay08:08:34

You have maybe 2-3 ways of looking at something. Write the 2-3 components and stop worrying about it, or use computed

tony.kay08:08:03

when do you only show a user’s age?

tony.kay08:08:10

it’s not a real example

tony.kay08:08:35

maybe you show a user’s name with their photo in a row: PersonRowView. Then you show their full profile detail UserProfile…etc

tony.kay08:08:55

We write that kind of stuff in UI code all the time. Write an iOS app. It’s exactly what you do.

tony.kay08:08:52

In your modal case: Maybe it is OK to do what your doing, because the data cannot change. If you understand that, then it is perfectly fine to do it that way.

tony.kay08:08:51

Maybe you even have modal-specific properties that you write into as you run the mutation to show the modal: (transact! this [(show-person-status-modal {:name ~name :age ~age ...})])

tony.kay08:08:33

you don’t care if the person changed behind the scenes…you’re showing a modal that is meant to be transient…don’t even make the modal aware of the data model beyond params.

tony.kay08:08:09

All sorts of options. Just understand the component query/ident/render model. If you understand that, then constructive “breaking of the rules” is fine, because you understand what can and cannot hurt you.

claudiu09:08:18

Thank you. Makes a lot of sense. Was just trying approaches. I really have a problem with the JSX & how some people use it, trying to avoid that style where the render could be just 20 lines of linear code but because of conditionals people break it up in 10 small functions that just do a if and render 1 line (usually end up split screening the same file just to be able to fallow the flow) 🙂

tony.kay09:08:09

I agree. I’d rather see a few small components than one big convoluted one. Single-responsibility.

tony.kay09:08:44

Design is always a fine balance of convenience, conciseness, and praticality.

currentoor16:08:14

Also, is there a way to run the full template in production mode locally?

tony.kay17:08:04

@currentoor Advanced compilation has been working for a while now…the problem is caused if the production compile includes the dev source and resources. specifically, calling tools.namespace.repl/refresh is what breaks it.

tony.kay17:08:23

and the dev source files had a call to that at the top level, so it was breaking the compiler hack in Om

tony.kay17:08:36

Fixing the source path with a profile for production builds fixes it.

tony.kay17:08:58

To run the template in production mode: Just manually build the production cljs build, and run the server.

tony.kay17:08:05

it’s the same js filename

currentoor20:08:32

@tony.kay i’m trying to build the devcards with advanced compilation on (for visual regression testing)

currentoor20:08:23

to :advanced or :simple the devcards compile just fine with lein with-profile dev cljsbuild once cards

currentoor20:08:44

but they don’t render in the browser

currentoor20:08:10

it’s like they don’t mount, do you have any idea what causes devcards to mount and why it wouldn’t happen in other compilation levels

tony.kay20:08:19

No figwheel?

tony.kay20:08:37

the docs on devcards require an explicit start of the UI if you’re not using figwheel to run them

currentoor20:08:53

oh right, I had that in boot

tony.kay20:08:04

:devcards true has to be added in the compiler section, too

currentoor20:08:15

yeah i added that one

tony.kay20:08:16

so, not related to optimization 🙂

currentoor20:08:32

awesome thanks for the help

currentoor20:08:42

getting close to switching completely over to fulcro

roklenarcic21:08:09

I forgot... why do we need to use computed instead of props for parent passed things?

roklenarcic21:08:42

What happens if I call df/load-action twice in a mutation, followed by a state swap? Is it executed sequentially?

wilkerlucio21:08:10

@roklenarcic state swaps that requires information from load should be called in a post-mutation

wilkerlucio21:08:27

the loads will be serialized to the server

roklenarcic21:08:04

can I accomplish recursive loading with post-mutation?

roklenarcic21:08:51

I have a tree where whenever someone clicks an item I need to load all the ancestors recursively