Fork me on GitHub

@adamvh be aware that local state is evil, you rarely have a good reason to use it, and later you probably regret... any reason to don't use the app state instead?


well, i've since re-worked it to use the app state, but, my thinking runs this way:


aren't components with local state a little bit more re-usable?


since they don't have to know anything about the app state?


if you give idents to your components, they always know where their own state is, which makes then highly re-usable


i'm trying to implement a datepicker as a fulcro defsc


and there's a bunch of stuff a datepicker wants to know (e.g. what month the user is looking at right now)


that seems like it would kind of be polluting the app state


put everything there, it's the best approach, this way you get the whole trackability story


so the pattern for a reusable datepicker would be something like


have :datepickers/by-id as a top-level thing in the app state


and then those would have ids


and maybe they would get mutations from computed


to execute when the user selects a date


you can make the whole management inside of the data picker component


and have the parent provide a callback to update the value


like a text field, or any input for that matter


I gotta leave now, but I suggest you check the sources on fulcro inspect:


there are lots of cool patterns on how to write and manage the components there


thanks for your help!


no problem 🙂


@tony.kay Thanks, but apologize, I cannot understand what you said well, the easy-server's make-fulcro-server made itself a build-in-component "handler", Is this "handler" just for web server? In the websockets demo, there is a custom "handler" which is for websockets, and there will have two "handler" components, if I put the websockets' handler in :components in the make-fulcro-server.


@tony.kay I just read the

(require '[ :as cs])

(defn make-system []
    :config-path "config/app.edn"
    :parser (prim/parser {:read logging-query :mutate logging-mutate})
    :parser-injections #{}
    :components {:channel-server  (cs/make-channel-server)
                 :channel-wrapper (make-channel-listener)}
    :extra-routes {:routes   ["" {["/chsk"] :web-socket}]
                   :handlers {:web-socket cs/route-handlers}}))


There is not a custom handler, instead, use :extra-routes.


@tony.kay I read the comments of the simple-channel-server, and I want to use defmutation, defquery-root. So I decide use the method in websockets demo to create system-map, and use :dependencies of the simple-channel-server to add db component in the parsing environment. It seems work. But I'm not sure if doing this ok?


That's exactly right @tpliliang


@tony.kay what is the :ref key in the env map for mutations? I don't see it mentioned in the dev guide anywhere, but I see it used in the source.


@adamvh the component ident.


specifically the return value of ident called on the first argument of the call to transact! that initiated the mutation?


and this is useful because in a mutation i can (assoc-in @state (conj ref :abitrary-key) new-value) to change :arbitrary-key in my component's props


got it, thanks


But what happens when you call the same mutation but without specifying anything for ref?


Oh right I see - exactly what you said @adamvh


So when you call transact! you can specifically set it (the ref is one of the arguments to transact!), or if you are calling transact! from a component and have not put in anything for the ref parameter then basically (ident this) will be used.


I guess when you call transact! passing in the reconciler rather than this is when you need to be explicit about what ref is.


Wonder what happens if you call transact! with a reconciler as first argument but without putting anything for the ref parameter??


am I doing something wrong when I’m thinking about introducing a component just to get a new ident?


I would say not at all @thheller


@cjmurphy and you can code your mutations to be defensive about the possibility of a nil ref, e.g. (conj (into [] ref) :keyword)


where you'd just assume that the top-level atom is your component's props


Got it - where keyword is a link (not atom). That makes sense - there's no ref, so the mutation (in this instance of the transact! call) has no relationship with a real (having an ident) component, so the join must be a link (i.e. not in a table). Is that way of doing it used in Fulcro Inspect?


@cjmurphy I always expect ref on my transactions, I used to use transactions that don't, but I learned that when you do that, you can't add multiple instances of that same component, making your component more rigid, so these days I add idents to everything that has state (except the root, which is usually a very thin component that just points the real root, which has an ident), this approach makes the components flexible to re-use


Yes I'm the same. I would go so far as to say I don't even like links! Even one-off configuration stuff I would put in a table with a suitable 'configuration' name. I'm more than happy to have plenty of tables with only one record (that the way I think of it) - like in your Fulcro Inspect source code where you use "main" as the id part of an ident.


and from perusing the inspect source code, the pattern for re-usable components seems to be to put them in a namespace and shove a table of them into the app state under the key :namespace/id


unlike in all the dev guide stuff where your component's props has a key :db/id and the app state has a thing :some-keyords/by-id


Yes it is interesting to see the mutations in the same ns as the components, and the use of :: - which means they have to be in the same ns.


i.e. all the defsc are in their own namespace and have {:ident [::id ::id]}


which seems like a good pattern to me if you want to make your component into a library


Personally I really like the convention of using :table-name/by-id. Harder for me to read without that convention.


yeah, but the key is that if table-name is the same as the namespace where your defsc lives, then it isn't going to collide with some random top-level key that client code happens to have put into app state


But yes having the :: means you can't have :table-name/by-id.


well you can ::by-id


and the only thing that really changes is you can't pluralize your namespace


Oh yes, okay.


But do you realise that there are many clients - as in many Fulcro clients?


Each tool will have its own reconciler, and its own root.


That will be a separate Root from the host/target.


So I don't see any problems with collisions.


you also give users an indication where this table in their app-state came from, more so than just picking a key


So I don't yet see a great need for using ::, unless you can't think of a name for your table - so you want your table name to be the same name as the ns you are in.


i think if you're trying to offer your component as a library it makes sense


that people who require the namespace your library provides can see stuff to do with your components under :your-components-namespace/by-id in their app state


But tools (not necessarily libraries, so we might be talking about different things) use a different reconciler. Fulcro Inspect can never name clash because it is a tool.


The only thing it puts in the target/host is an app-id.


ohhhhhh, I see what you're saying


I'm talking about generalizing the patterns in fulcro-inspect to produce general re-usable UI components


When you use Fulcro Inspect you can actually pick which 'reconciler' you want to look at the app state of. You will only notice that I guess if you have many clients, for instance with devcards you are adding Fulcro clients.


Right yes - make sense for reusable components.


@adamvh I would say one additional thing about ref in the environment. It can be useful, but it can be abused. Transactions are not always run from the thing that the mutation modifies. So, basing your mutations on ref is IMHO a bad idea for many things. I highly recommend passing the ID as a parameter. This encodes it in the mutation so that is clear what is being changed, and encodes it on the wire if the thing goes remote. It lets you use the mutation in a callback at some other place in the UI. You can use (transact! this ref tx), but that means you have to code a table name and ID instead of just an ID. YMMV, but I rarely use ref.


So your first example breaks based on where and how you call that mutation...making it only really good for internal mutations that are hidden within components and have no side-effects outside of that component.


Yeah, what led me to using ref was trying to implement buttons that change which month the user is looking at in a datepicker widget, which is kind of a component-local-state use-case to begin with. I imagine the component as something that component-external mutations interact with basically only by setting the current selected date (and maybe min / max allowable dates).


Right. Perfect place to use it


and :db/id: if it is a persistent entity in Datomic, that's what you use


I can see how passing the ID as a mutation param is a better general pattern (and also works here).


ah, right, i see


Using namespaced keywords gets you Clojure spec support


sorta like I did in the websocket demo


and database IDs rarely need more than one spec...they're usually 64 bit long or UUIDs


Yes I forgot that - that demo was a real eye opener for me as to how spec can work - providing you with names that have all that checking against them.


@cjmurphy @adamvh the reason I stopped using the by-id convention, is that when you do that, you have to translate that on your server from by-id to id, in Phatom connect I use the attributes to make direct linkable of entities, by dropping the by-id convention and using id directly I'm able to remove any conversion code from one to another, and that's why I prefer just dropping the attribute name directly there


@cjmurphy yeah...better than types. More accurate and flexible checks, but none of the inflexibility/boilerplace. Well, types can make things run, if hard runtime performance guarantees is of higher concern than correctness, simplicity, and cost of development...


and to be clear about refs, I think they great to use for operations that live on the client-side, when there is server involved I always send the proper id's as part of the params


@tony.kay can we start accepting empty body on defsc? there are cases we want the component just for the query, and havign an empty body currently doens't compile, so I end up having to write things like:


(defsc Comp [_ _]
  {:query [:a :b]
   :ident [:a :a]}
  (identity nil))


on the topic of defsc complaints, it'd be nice if it just kind of magically did the right thing if for :initial-state {:a map}(which is what :query [:a :vector] does, so it's not totally insane of me to expect this)


@wilkerlucio that’s been there since beta 5 or something


oh, thanks, working great 🙂


everything is optional except arg list


@adamvh That is also present. Read the docs…the children case is super-magical (not insane of you to expect it…since it already does it)


:initial-state {:db/id :param/id :child {}} ==> (fn [{:keys [id]}] {:db/id id :child (prim/get-initial-state Child {})}) where Child is derived from the join in the query


I’m not advertising that as much, because it is so magical as to be confusing for people


@wilkerlucio Changed your patch to ptransact!, which now has the exact calling API as transact! (accepts or derives ref). It is also fixed to work correctly in the presence of mixed local/remote sequences.


2.0.0-beta8-SNAPSHOT on clojars. Public API of pessimistic-transact->transaction extended to allow augmenting parsing env of mutations that are deferred. This is used by the main ptransact! to now make sure :ref flows through deferrals.


note that “template mode” is written to sanity check things. For most components it does the “right thing”. In some circumstances it is too aggressive…in that case, you can always drop back to lambda mode for the component that needs looser rules. (Examples are components with initial state and dynamic queries that don’t match, root components that want to add in extra stuff on initial state that they themselves don’t query)


oh ... that was more magic than i was expecting, which tripped me up 😛


i somehow didn't grok my first time through these docs that it was generating calls to get-initial-state for me


so i had them in there and as a result there was choking


yes, but it kinda has to in order to do the checking…otherwise you have to worry about general code evaluation


I should add an error check that tells you that


Please open an issue on that…I need to get on the road


aight, will do, thanks for your help, tony!