Looking at some old code there were a couple of undesirable features. One particular code smell is a good motiviating example: handlers calling other handlers.
It encourages the developer to "register handlers as reusable components" but that's a pretty clumsy way to write utilties. Over time those util handlers were collecting barnicles and were becoming very hard to reason about or refactor.
With the advent of :fx
we have a clean composable alternative with some desirables traits.
My app has three namespaces (app.handlers, app.actions and app.utils)
Handlers have the job of triggering a bunch of simple composable actions. They are all reg-event-fx handlers so actions can be used to layer up behaviour.
There's a few important parts to their role
- deal with the re-frame plumbing (e.g. unpacking the args to pass to actions as needed)
- invoke a set of actions (e.g. the composition bit)
- event validation (e.g. ignore a stale response from a slow request)
- flow control (e.g. different actions for anomalies...)
They're simple and not cluttered with code implementing behaviour so are easy to read.
I dont try and reuse the handlers for multiple events since that would make it harder to add event specific behaviour later.
(defn -bootstrap
[_ [_ path]]
(-> {:db {}}
(actions/setup-for-path path)
(actions/load-logger-allocations)
(actions/load-samplefiles)
(actions/load-sites)
(actions/load-contenttypes)))
(defn list-scroll-end
[{:keys [db]} [_ list-id row]]
(-> {:db db}
(actions/list-set-row list-id row)
(actions/list-get-page list-id row)))
Note: it's quite likely that setup-for-path used list-get-page to fetch data for a list too.
Actions are little, composable building blocks for behavour.
The first arg is "state" which is passed through. Most actions will update :db
or :fx
. The result is a valid response from a reg-event-fx handler.
(defn init-list
[{:keys [db] :as s} route query]
(-> s
(assoc-in [:db :list route] (utils/init-list-state db route query))
(list-get-page route 0)))
This one shows an additional :fx being added
(defn list-get-page
[{:keys [db] :as s} route page-id]
(let [list-props (get-in db [:list route])
{:keys [page-ids]} list-props]
(cond-> s
(and page-ids (not (contains? page-ids page-id)))
(-> (assoc-in [:db :list route :page-ids page-id] :loading)
(update :fx conj [:app/go-req
{:req (table-utils/get-page-req list-props route page-id)
:resp-v [:app/-list-get-page-response route page-id]}])))))
To reiterate in spec form
(s/def ::state (s/keys :req-un [::db ::fx]))
(s/def ::action (s/fdef :args (s/cat :s ::state :... (s/* any?)) :ret ::state))
All bets are off here. No required conventions. Having this separate means the app.handlers and app.actions namespaces have very consistent conventions.
(I think @p-himik suggested this to me. Hope I have that right.)
Now we've ditched "utility handlers" handler registration can be associated with clear events (e.g. user clicked save, fetch golem returned data)
Suddenly your re-frame debug message are a clear log of interactions around your system... no guessing where that ::load-x event was dispatched from.
I value the conventions but others might prefer to avoid adding new namespaces for "actions" and "utils".
Could argue these were convenient to isolate and test. Some may miss this.
I register unique event handlers for unique user/golem events. This means more handler registrations. Not a lot more though.
With this we have a lovely clear event log to debug.
Each handler composes many bits of behaviour now instead of dispatching to some other more specific handler.
By rights these handlers are more complex. That might be a downside for debugging.
It can be inconvenent to write flow control while threading state through.
I think this pain point has led to the range of 'better cond' macros:
Where this niggles I've noted that breaking the action down helps. (e.g. an action for each branch).
How does this address when there are dependencies among the actions that may have async operations involved? One action may need to "wait" for other async actions to complete before proceeding.