Skip to content

Instantly share code, notes, and snippets.

@olivergeorge
Last active Sep 8, 2021
Embed
What would you like to do?

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.

Handlers, actions and utils

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)

app.handlers

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

  1. deal with the re-frame plumbing (e.g. unpacking the args to pass to actions as needed)
  2. invoke a set of actions (e.g. the composition bit)
  3. event validation (e.g. ignore a stale response from a slow request)
  4. 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.

app.actions

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))

app.utils

All bets are off here. No required conventions. Having this separate means the app.handlers and app.actions namespaces have very consistent conventions.

Trade-offs

Each event is clearly associated with a user interaction or golem respose

(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.

More namespaces

I value the conventions but others might prefer to avoid adding new namespaces for "actions" and "utils".

Less utility handlers

Could argue these were convenient to isolate and test. Some may miss this.

More "real" events

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.

Handlers do more

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.

cond-> s pattern is limiting

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment