Events, Effects, Coeffects, Interceptors
reg-event-db vs reg-event-fx
reg-event-db:(fn [db event-vector] new-db). One coeffect (db), one effect (new db). Pure. Default choice (~90% of handlers).reg-event-fx:(fn [cofx event-vector] effects-map).cofxalways contains:dbplus any injected coeffects. Use only when the handler needs extra world inputs (now, random, LocalStore) or must cause non-db effects (HTTP, dispatch, timers).
(reg-event-db :auth/set-token (fn [db [_ t]] (assoc-in db [:auth :token] t)))
;; equivalent -fx form:
(reg-event-fx :auth/set-token (fn [{:keys [db]} [_ t]] {:db (assoc-in db [:auth :token] t)}))
Both take an optional interceptor vector as second arg: (reg-event-fx :id [interceptors] handler). Both silently prepend standard interceptors: do-fx (actions the effects map at chain end) and (inject-cofx :db) (why :db is always present).
Common mistake: returning modified cofx from a -fx handler ("no handler registered for effect: :event"). Return a fresh effects map: {:db (update db :clicks inc)}.
Effects
Effects map: each key names an effect, value is its payload. Since v1.1.0, return only :db and :fx:
{:db new-db ;; always actioned first
:fx [[:dispatch [:some/event "extra"]] ;; then :fx entries, in order
[:http-xhrio {:method :GET :url "..."}]
(when condition [:full-screen true])]} ;; nils ignored → conditional effects
Built-in effects:
:db— reset app-db to the value.:fx— vector of[effect-id payload]tuples, actioned in order; nils skipped.:dispatch— one event vector. For multiple, use multiple:dispatchtuples in:fx.:dispatch-later—{:ms 200 :dispatch [:event-id "param"]}.:dispatch-n— deprecated since v1.1.0.:deregister-event-handler— event id(s) to remove.
reg-fx (custom effects)
(reg-fx
:local-store/save ;; effect key
(fn [{:keys [key value]}] ;; called with payload; return value ignored
(.setItem js/localStorage key (pr-str value))))
- Keep effect handlers dumb — no logic. They are the impure edge; hard to test.
- Data-less effects take
nilpayload:{:exit-fullscreen nil}. - Document the payload shape — each effect is a nano-DSL.
- Effect handlers live in
.cljs(they touch the platform); the event handlers describing them stay in.cljc.
Coeffects
Coeffects = data the handler needs from the world (time, localstorage, random). Inject them as data so the handler stays pure and testable.
(reg-cofx :now (fn [cofx _] (assoc cofx :now (js/Date.))))
(reg-cofx :local-store
(fn [cofx k] (assoc cofx :local-store (.getItem js/localStorage k))))
(reg-event-fx
:settings/load-defaults
[(inject-cofx :local-store "defaults-key") (inject-cofx :now)]
(fn [{:keys [db local-store now]} _]
{:db (assoc-in db [:settings :defaults] local-store)}))
In tests, re-register the cofx with a fixed value ((reg-cofx :now (fn [cofx _] (assoc cofx :now fixed-date)))) and restore with make-restore-fn (see testing reference).
Interceptors
Chain of maps wrapping the handler — data-based middleware. A context {:coeffects {...} :effects {...} :queue ... :stack ...} threads forward through every :before (builds coeffects; handler runs last, return goes into :effects), then backwards through every :after (processes effects; do-fx actions them at the very end).
Built-ins (all from re-frame.core):
debug— logs event +clojure.data/diffof db before/after. Dev only:(when ^boolean goog.DEBUG debug).trim-v— drops the event id so handlers destructure[id & args]without the leading_.unwrap— for map-payload events[:event {:x 1}], hands the handler the map directly.(path :cart)— handler sees and returns only that subtree of app-db (likeupdate-in).(after f)— run(f db)for side effects post-handler; ideal for schema validation. For-fxhandlers:dbmay be absent:(after #(when % (validate! %))).(enrich f)— post-handler db computation/validation.on-changes— precursor to Flows.
Interceptor vectors are flattened and nils removed — compose freely: [standard-interceptors (path :cart) trim-v].
Custom interceptor:
(def log-event
(rf/->interceptor
:id :log-event
:before (fn [context]
(js/console.log (rf/get-coeffect context :event))
context)))
Global interceptors (run for every event): (rf/reg-global-interceptor log-event).
Talking to servers
Pattern: view dispatches intent → -fx handler sets a loading flag and describes the request → success/failure callbacks are further dispatches (never close over db in async callbacks). Use day8.re-frame.http-fx (requiring the ns registers :http-xhrio):
(ns app.cart.events
(:require [ajax.core :as ajax]
[day8.re-frame.http-fx]
[re-frame.core :as rf]))
(rf/reg-event-fx
:cart/fetch
(fn [{:keys [db]} _]
{:db (assoc-in db [:cart :loading?] true)
:fx [[:http-xhrio {:method :get
:uri "/api/cart"
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:cart/fetch-succeeded] ;; response conj'd on
:on-failure [:cart/fetch-failed]}]]}))
(rf/reg-event-db
:cart/fetch-succeeded
(fn [db [_ response]]
(-> db (assoc-in [:cart :loading?] false) (assoc-in [:cart :items] response))))
Always register the failure handler too.
Loading initial data
app-db starts as {}; only event handlers change it — even initial values:
(rf/reg-event-db :app/initialise (fn [_ _] db/initial-db))
Simple app — dispatch-sync before render so state exists before any view mounts:
(defn init []
(rf/dispatch-sync [:app/initialise])
(mount!))
Async data (from servers) — plain dispatch + a guard component:
(rf/reg-sub :app/initialised? :-> #(not (empty? %)))
(defn top-panel []
(if @(rf/subscribe [:app/initialised?])
[main-panel]
[:div "Loading..."]))