This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-02-28
Channels
- # announcements (1)
- # beginners (43)
- # calva (7)
- # clojure (48)
- # clojure-europe (19)
- # clojure-nl (1)
- # clojure-norway (24)
- # clojure-uk (4)
- # clojuredesign-podcast (4)
- # clojurescript (11)
- # conjure (15)
- # core-async (1)
- # cursive (1)
- # datomic (33)
- # events (1)
- # fulcro (2)
- # humbleui (21)
- # hyperfiddle (34)
- # introduce-yourself (1)
- # joyride (24)
- # lambdaisland (8)
- # lsp (3)
- # malli (30)
- # meander (2)
- # observability (5)
- # off-topic (2)
- # pathom (3)
- # polylith (26)
- # portal (5)
- # re-frame (28)
- # shadow-cljs (7)
- # spacemacs (2)
- # xtdb (6)
I notice that I want to give re-frame subs and events special suffixes so I can separate them from normal keywords. For example, naming as sub :report-sub/name
instead of :report/name
and report-fx/update-name
instead of :report/update-name
. Anyone else doing this? Bad idea?
I think even the docs mention this approach, although without recommending it. I myself have stopped using it after switching to ns-based kws.
Do you ever use the same keys in app db?
Basically, I find that the same key is used for multiple things, including subs, which makes renaming harder and adds some confusion.
So you never find that a key in app-db and the corresponding sub would naturally have the same name?
Now that I'm at my PC, I can write out a concrete example:
(ns app.user.preferences
(:require [re-frame.core :as rf]))
(rf/reg-sub ::value
(fn [db [_ k]]
(get-in db [:user :preferences k])))
Oh, so you use auto-namespaced keywords for subs? Then there is of course very little risk for collision. I have found ::sub-name
to be cumbersome when restructuring my code. I have instead used more "domain"-like namespaced keywords, like :data-loader/queue
, but have then also found that the app-db key and sub name often collide
(rf/reg-sub :data-loader/queue
(fn [db _]
(:data-loader/queue db)))
But maybe it should really be (assuming that there are other :data-loader/
keys)
(rf/reg-sub :data-loader/queue
(fn [db _]
(get-in db [:data-loader :queue])))
Also, I use domain-namespaced keywords for my entities, so for example
(rf/reg-sub :report/name
(fn [db [_ id]]
(get-in db [:reports id :report/name])))
Which is also a "collision" - but maybe it makes sense that the attribute and sub share name?> I have found ::sub-name
to be cumbersome when restructuring my code
If your IDE lets you search for particular keywords (as opposed to a plain full text search), then it's a matter of replacing an entry in a few :require
lists.
So you are all-in on ::sub-name
? 🙂
Gotcha. Thanks. You should really write a long blog post about your re-frame best practices and lessons learned.
I came to this channel and searched a few messages back to find more context on exactly the kind of pattern you describe @U2FRKM4TW.
In my current app I took the namespaced-kws approach for handlers and subscriptions. I have separated those into scene/screen/panel specific namespaces and I'm a bit confused as to how to manage intermediate states which tend to occur between or during scene transitions, something I refer to as the "loading spinner" problem.
"loading spinner" as in : in some namespace I have an event handler doing this
{:db (assoc db :loading true) ::panel1/load-stuff ...}
where the ::panel1/load-stuff
is responsible to handle the side-effects and dispatch another event to set the loading key to false when it is done. And as you see, it is in another namespace.
Then this loading key lifecycle is scattered over multiple namespaces and I don't like it. I hope it makes sense.
I have been trying to find blog posts or other resources showing examples of re-frame project structures other than the trivial example or starter app but I failed. The closest thing I could find to help solve my problem was Eric Normand's "https://ericnormand.me/guide/optimistic-update-in-re-frame" pattern. Which is more about how to avoid my problem than how to address it.
Would you have a few minutes to spare and describe what structure you use and how you manage to keep track of who is responsible for what (sub) state?
Why not just {::panel/load-stuff ...}
and make that event handle both (assoc db :loading true)
and (assoc db :loading false)
(the latter, presumably, in some "util" event triggered by some response to a data request)?
I also tend to avoid having global :loading
flags.
What usually happens in my code:
1. There's a view component for a panel with an async loading procedure, and that view knows about "ready" and "loading" states and renders them accordingly
2. There's an event that must make that panel appear - it sets the initial state, including the "loading" flag, and triggers the async workflow necessary for that panel
3. The final step of the async workflow set the final state and the "ready" flag
If that panel is a part of some parent panel and switching that inner panel must mark the parent panel as "loading", then that async workflow must overall be controlled by the parent panel. Alternatively, the "loading" state of the parent panel must be determined by its own state plus all the states of all the initialized children panels.
A couple things worth nothing:
• With async workflows you usually want to pass around some data indicating the "epoch" or "fingerprint" of the panel. Otherwise, you might see behaviors like "open panel -> close panel before the data is loaded -> the panel is opened by itself, triggered by the data finally being loaded" (the "open" flag being calculated based on the presence of the necessary data) and "open panel -> set different state (or close and reopen) -> see the new data -> see the old data" (a race condition). The docs of this library go into some particular examples: https://github.com/lucywang000/clj-statecharts
• Keep in mind global interceptors (and keep an eye on flows that are currently alpha). You can do a lot with them - things like "clients of a panel only set the 'open' flag, the rest happens automatically - regardless of how the flag is set" or "hiding a panel automatically cancels every associated async workflow that's in-flight".
Lately, I've been actively using a concept of, I think, my own invention - changesets.
Prime characteristics:
• Normalized data with a known schema of entity relationships
• Data patches with distinct IDs that can be applied independently - changesets themselves
• Every changeset can have metadata attached to it
• Every changeset can have editing history attached to it
• There's a way to register entity change handlers, so that e.g. when :item
with :id
7 is removed, all associated :sub-item
s with :parent-id
7 are also removed (just a convenient wrapper around global interceptors)
• Every action on a changeset first checks that the changeset is present
This enables:
• Combining and isolating history (undo/redo not per field and not per app, but per panel)
• Automatic handling of panel state (a panel component can be always mounted with an associated changeset ID :the-panel
, it's only visible if the changeset exists, its in-progress/error/warning/done/whatever state is stored in the changeset metadata; automatic start of data loading isn't handled, but trivial to do with a global interceptor that checks that a new changeset with that ID was added)
• Automatic cleanup of all the panel-specific data when it's closed (the whole changeset is simply removed)
• Unified API in terms of events and subscriptions
It's not quite ready for a public release though. But maybe one day.
You gave me a lot of good pointers. The "epoch" and global interceptors points in particular are great food for thought. I didn't really do "an event that must make that panel appear" given that I'm actually in re-dash, a re-frame inspired framework for ClojureDart, so I relied upon Flutter navigation push/pop stack directly rather than event for scene/panel transitions (I leaved out that bit of context in my question). That introduced a mismatch between the flow of events in re-dash and the Flutter stack. I see now that I'd better manage the stack in effects and keep the loading/ready state local to the panel. Thanks a lot!
Is there a way to access the re-frame app-db value directly without using subscriptions (and helper functions) in event handlers? What do you do? It seems that this is not approved according to the re-frame official documentation. Also, I am looking for alternatives besides the examples in the official docs. https://day8.github.io/re-frame/FAQs/UseASubscriptionInAnEventHandler/
The app-db itself is readily available to event handlers.
If you mean a value that's produced by a subscription, then I myself usually use a global interceptor for that. Such an interceptor reacts to the changes of relevant values in app-db, computes the desired derived value, and puts it under some key into app-db. The subscription then becomes a plain get
of that key. And any event can simply retrieve that value from app-db as well.
In the alpha version of re-frame, there's a new flow functionality that does pretty much the same as that global interceptor, but with much less boilerplate.
Thanks! I liked this approach. Do you have a code sample that demonstrates it? Perhaps a gist or something similar, if it's easy to share and the code is isolated.
Alas, no. But there's very little to it, it's mostly how the built-in on-changes
interceptor works.
I don't put derived values into app-db unless an event handler also needs it. 2nd- and 3rd-level subs are only recomputed when their inputs change, so it's very efficient for display purposes. But again, if it ends up being something needed, e.g. to submit to the server, then it's much easier to use a mechanism to update app-db when inputs change; either when the new data was fetched or by interceptor.
Hi there, I keep finding instances in our codebase where people call (dispatch..)
in the hot rendering path, often at the beginning of a hiccup function.
In almost all cases this seems to be wrong.
Is there maybe a good trick to WARN on the console if someone is doing that?
i.e. could dispatch know (assuming I'm re-deffing it) if it was called inside a reagent/react render loop?