After some time away, I’m back to re-frame and loving it. I’m beginning a non-trivial (not a toy) project, and I’m trying to put some serious thought into naming conventions. On the view side, I have components (form, table, clickable button, etc), containers (subscriptions to data, passed down to child components), views (groups of containers), and pages (with a few views). The app is mostly tables, forms and eventually PDF reports (non-re-frame of course). Is that too many moving parts?
I want a naming scheme that isn’t all over the place, where I can tell at a glance if a keyword is an effect, a subscription, a view, etc. while at the same time avoiding duplication and overly verbose names.
So a successful http call would dispatch :account/data and :user/data rather than :event/account-data and :event/user-data, for example.
Any other suggestions, best practices, blog pages on the topic, etc?
@p-himik The way we do it with our product, we only have views.cljs
and control.cljs
, where the control is responsible for all events, data, etc. and turning it into an easier representation that can be called out to by the views. While it does sometimes run into the issues that you describe (i.e. if we have extremely simple events / getters and setters), I think it's a bit less cumbersome than the 4 different namespaces described above, and the structure does help break things up, and keep things a bit modular.
Just mentioning this because I'm also a bit curious as to how you organize things personally / how you recommend doing this. Do you simply have one ns for each Component / Page in the application? I've primarily used similar patterns that break things up like this (MVC, etc.), and it usually does help keep code cleaner and a bit more modular, but wondering if it's not as much of an issue in a Clojure based stack.
> Do you simply have one ns for each Component / Page in the application? Yes, with some justified ad-hoc exceptions.
Interesting, I'll keep that structure in mind then!
What I’m aiming for is smart re-use. So a clickable button, for example, can be styled, etc but you pass in the event you want sent with the click handler, plus any relevant args grabbed from immediate component context. A table is more complex, but could be similar with a vector of column headers a vector of row maps. Anything special can be an event passed in and sent off with the dispatch call. Then a container could fire off events, subscribe to results, and pass those atoms down into components. Pretty basic stuff, really.
But I want to make sure the namespaced keywords are an asset rather than an annoyance when it’s time to refactor.
I wouldn't put refactoring costs too high on the priority ladder. Otherwise, abominations AbstractSingletonProxyFactoryBean
would appear. :)
Both kinds of keywords are just keywords. You refactor them like anything else, especially if your development environment treats them as first-class citizens.
The only way namespaced keywords can become cumbersome is if you move a keyword like ::x
into a different namespace. Maybe your DE supports that just fine, and then there are no problems whatsoever.
Maybe mine IDE supports that as well, but on average I move about one keyword every 2-3 months I'd say, so I never bothered to even check.
So with a separate ns for each component and page, where do you get your reuse, particularly of components?
Not sure if this is exactly what you're going for @jmckitrick, but here's a snippet of what it would look like for us:
(ns example.callback-button
(:require [reagent.core :as r]))
(defn CallbackButton
[]
(let [num-times-clicked (r/atom 0)]
(fn [{::keys [callback text]}]
[:button {:on-click #(do (swap! num-times-clicked inc)
(callback))}
(str text " " @num-times-clicked)])))
(ns example.main-page
(:require [example.callback-button :as cb]))
(defn Component
[]
[cb/CallbackButton #::cb{:text "A button"
:callback #(print "A callback")}])
(defn AnotherComponent
[]
[cb/CallbackButton #::cb{:text "Another button"
:callback #(print "Another callback")}])
With most of the complexity of the component in the respective ns, and other pages / components simply pass the high level info needed
> So with a separate ns for each component and page, where do you get your reuse, particularly of components?
Not sure I understand the question. You have e.g. a my-proj.calendar
ns, and you use it just as is, as a whole.
If you need to share e.g. a single event handler function out of that ns, then you have to think what kind of dependency that is. Is it something that belongs to the calendar but is just used some place else, or is it something just common for different things? If the former, you don't extract that function and just keep using my-proj.calendar
. If the latter, you extract it into my-proj.common
or whatnot.
I’ll have to do some experimenting and see if I can come up with concrete cases.
Maybe your approach of keeping all events and subs with their components eliminates the issue entirely.
@p-himik So just to be clear, you’ll have a ns with views/components, and all events and subs are in that namespace, unless factored out for common use elsewhere. So that means namespaced keywords are resolved right there, in those namespaces.
Indeed.
If an event is used somewhere else, I just
(require '[my-proj.calendar :as calendar])
(dispatch [::calendar/set-date "2020-10-21"])
or something like that.And you generally group the view, event, and subscription all in the same namespace and file?
What about cases where, say, an http fx is used in different places? I guess a ‘common’ namespace or is there a better way? @p-himik
Yep. And yes, just a common ns. Sometimes people extract fxs in a separate special fx
ns, I tend to do the same for well-focused tiny effects and I create specialized namespaces for effects that serve the same purpose.
IMO it's comparable to just writing functions in Clojure, which is (again, IMO) not that different from writing code in any modern and actively used language. Well, Clojure does have some polymorphism which may create some differences, but re-frame related stuff is not polymorphic so it's more similar to other languages.
I have quite a lot of legacy code that does split functionality into views/subs/events, and I'm gradually refactoring it into single namespaces. Working with them, in my experience, proves to be much easier. And simpler, I think. :)
Ok, that’s the route I’ll try… for some reason I like the idea of separate folders (views, pages, components, etc) because it seems better organized for navigation and reuse, but things are not always as they seem….
I just use FQN with the current namespace, i.e. every event or subscription keyword ID always starts with ::
.
I know somewhere I read a reasonable argument against that approach, but I forget what it was. I could still go either way this early in the project.
Have you run into any drawbacks?
Just a very minor one - it makes it slightly harder to move event handlers and subs around. But I don't think it really matters.
Now that I think about it, it might have been related to spec more than re-frame…. I’m not sure.
But your comment makes sense. I’ll give that a shot.
I’ve had good experience with putting each “sub-page” under separate folder that has
• routes.cljs
• view.cljs
• events.cljs
• subs.cljs
Each sub-page is also a namespace
Oh, yes, I recall. double-colon namespace keywords are harder to search for in your codebase. I think that was it…..
You can always write them without the ::
shortcut. 🙂
Regarding separating functionality into views/events/subs files - I don't do that. Frankly, I think it's counterproductive.
E.g. suppose you have a simple sub that's implemented using just get-in
and that's used by a single view. And the event that sets that value is implemented using just assoc-in
.
Now why would you ever want to separate these things? Usage is in one file, setter is in another, getter is in yet another one - it makes it much harder to navigate and get a glimpse at how something is working.
@jmckitrick Regarding "harder to search" - do you use for Clojure development? Cursive can absolutely search for such keywords. I don't know for certain but I'm about 90% sure that VSCode, Vim, and Emacs plugins fur Clojure also support searching for such keywords.
@p-himik CIDER might have caught up as well
how do ppl handle testing of http xhrio effects? i want to write a test with mocked ajax responses.
I would just replace the URL with something that guarantees the required HTTP code.