This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-08-05
Channels
- # announcements (7)
- # babashka (9)
- # beginners (47)
- # calva (28)
- # clj-kondo (17)
- # clj-otel (20)
- # clojure (193)
- # clojure-brasil (1)
- # clojure-europe (43)
- # clojure-norway (12)
- # clojure-uk (6)
- # clojurescript (18)
- # datalevin (15)
- # figwheel-main (3)
- # honeysql (3)
- # hyperfiddle (44)
- # introduce-yourself (2)
- # java (10)
- # lsp (19)
- # malli (9)
- # meander (4)
- # off-topic (14)
- # polylith (48)
- # re-frame (21)
- # releases (3)
- # shadow-cljs (6)
- # tools-deps (29)
- # yamlscript (3)
Hi, are there any articles about structuring a large re-frame application (specifically file organization, and event / sub naming)? I saw https://github.com/day8/re-frame/blob/master/docs/App-Structure.md but looking for something a bit more in-depth. Some things I'm thinking through are: 1. Where to put component specific subs / events (one thought is either the same namespace as the component or a namespace beside the component). 2. How to share logic between events & subs (feels like this would be easier if subs & events lived in the same namespace). 3. How to further break down pages and organize its events / subs. Current project has separation by page but some of these pages are pretty large. Appreciate any insight!
I have this same problem. There are some solutions in the re-frame channel, if you search, but there’s nothing really on the web to address this. I look forward to hearing what others have to say.
I haven't seen such articles but there have been plenty of discussions here around those topics.
1. re-frame docs tend to separate things into separate namespaces, like component.subs
, component.events
, component.views
. I used that approach initially but quickly switched to just component
since it makes a lot of things much simpler. Of course, I split it into sub-components when needed and extract parts that other namespaces need into their own namespaces, like component.common
2. Depends on what exactly is shared. If it's a simple computation that can be expressed as a pure function, then yeah, just do that and put it somewhere where both the subs and the event handlers can access it, which is indeed easier if they're in the same ns. But if that computation requires reactivity (e.g. conditional signal subs), things get more complicated. Flows (currently in alpha) might help here, as well as global interceptors. I ended up creating custom wrappers for reg-sub
and subscribe
with an addition of a dynamic var that all together allowed me to create a sub coeffect without the potential issue of sub leaks via the cache
3. The question is too vague and not that different from "how do I write good code?". :) I myself have it at the intuition level so can't provide a succinct advice without having to think about it too much, except a similarly vague and generic "do only what makes sense"
Appreciate all the insight! Will search the channel for other discussions. I was wondering if there were certain patterns that projects always follow for consistency but it sounds like breaking apart / combining namespaces is done as it makes sense at the component or page level.
A couple of other things:
• I name all subs and events with ::
at the front, so they're all fully qualified with the namespace where they're defined. And then if I ever need to use such a sub/event in another namespace I just require the ns with the definitions as [the.ns :as x]
and use the ::x/...
aliasing. It makes sure that the namespace is indeed loaded and it becomes trivial to see where a particular sub/event comes from, just like with regular functions. Some people might argue that it makes moving reg-*
calls around more difficult, but at least in Cursive renaming a keyword is very straightforward
• I create wrappers for most re-frame functions that allow shortening the most common usages. E.g. for a get-in
style of sub I can do something like:
(my-reg-sub ::-dialog :-> ::dialog)
(my-reg-sub ::dialog-title :<- [::-dialog] :-> :title)
(my-reg-sub ::dialog-some-nested-key :<- [::-dialog] :-> [:some :nested :key])
Thanks @U2FRKM4TW this is very helpful! I have been going back and forth about using fully-qualified keywords for events and subs myself.
I also want to experiment with turning things like (rf/subscribe [::some-ns/sub-id arg1 arg2])
into (some-ns/sub-id arg1 arg2)
, so that I can more easily navigate around and see inline help for a sub, including its arity. Same for events.
I use this structure, foobar
being a component, view or other domain specific grouping of connected functionality:
- foobar.core
- foobar.constant
- foobar.db
- foobar.spec
- foobar.sub
- foobar.event
- foobar.cofx
- foobar.fx
- foobar.view
- foobar.util
If I know the scope is very small, I might start with core
and extract things as needed.thanks for sharing @U0DHDNZR9! do you re-export the view from foobar.core
so consumers just reference that one namespace?
No, I reference things where they are e.g. (:require [foo.bar.foobar.view :as foobar.view])
and [foobar.view/component]
I use :as
to qualify everything so I know where it comes from, rather than :refer
Another quick question, if you reference things where they are what do you have in foobar.core
? Seems like all a component's functionality would be covered by those other namespaces
@U2U78HT5G One example would be to load things that are just for side effects e.g. third party js libraries, and then i load the core namespace in the apps root
(1) We typically split our “views” from our “handlers”. We mostly do this so we can use .cljc
for the handlers - to test on the JVM side if we want (often just for convenience around a few things). There are more parts to our standard layout than this, but this is the main re-frame relevant point.
So all sub, event, coeffect, and effect handlers are in a handlers ns like project.handlers.subject1
and the that’d usually be used by a view called project.views.subject
but sometimes we share a handlers ns across views for common stuff etc.
(2) Also, I have a sophisticated macro over rf/reg-sub
that simplifies some common patterns that we found clunky and error-prone with the fn and we added one novel feature that is pretty useful.
The macro looks like:
(r/regsub ::my-sub
{:q [arg]
:let [x ::other-sub
y [::another-sub 10]
z [::another-sub arg]]}
(do-something-with arg x y z)
Where this sets up the appropriate signal graph - via the :let
naming the values coming from other subs.
We extended this further to support a get-in
path style, but with optimizations on the mechanics underneath that are quite useful for perf considerations. It has the form:
(r/regsub ::my-sub
{:q [arg]
:let-path [x (my-path-for arg)
y [:static :path]}
(do-something-with arg x y )
Where each :let-path
entry registers and uses a chain of subs that each subscribe to the next component along the path, So if your path is [:x :y :z
]` it automatically creates a sub for :x
, then a sub that depends on that and does get
:y
and then one that depends on that and does get
:z
.
We also detect when we’d be registering a sub we already registered for this path making - and we reuse instead when possible.
This makes an efficient chained signal graph, where subs aren’t reactive to more than they actually care about, but cuts out a bunch of boilerplate. Also, the :let-path
lets us use the same paths and path helpers we use in the event handlers to update/access parts of the app-db.I like this idea https://clojurians.slack.com/archives/C073DKH9P/p1722874617559589?thread_ts=1722864507.061879&cid=C073DKH9P Something worth considering definitely.
To me the standard approach of putting all subs together and all events together etx was like Ikea furniture where you get 32 wingnots and 32 washers etc etc. I find colocation suits how I work, and is more appropriate for the Cloure Way.