Patterns and Gotchas
Bootstrap / entry point (React 18 Reagent)
(ns app.core
(:require [reagent.dom.client :as rdc]
[re-frame.core :as rf]
[app.events] ;; side-effecting registrations — MUST be required
[app.subs]
[app.views :as views]))
(defonce root-container
(rdc/create-root (js/document.getElementById "app")))
(defn mount! [] (rdc/render root-container [views/root]))
(defn ^:dev/after-load reload! []
(rf/clear-subscription-cache!)
(mount!))
(defn init []
(rf/dispatch-sync [:app/initialise])
(mount!))
Reusable components
Two kinds:
- Reagent components — single-value widgets; I/O via args (value in, callback out). Use for libraries.
(defn text-input [value on-change]
[:input {:type "text" :value value
:on-change #(on-change (-> % .-target .-value))}])
- re-frame components — entity-level; input via
subscribe, output viadispatch. Multi-instance pattern: pass an identity (entity id / path segment) and parameterise both query vectors and events with it:
(defn customer-name [id]
(let [customer @(rf/subscribe [:customers/by-id id])]
[text-input (:first-name customer)
#(rf/dispatch [:customers/change id :first-name %])]))
;; list: (for [id @(rf/subscribe [:customers/all-ids])] ^{:key id} [customer-name id])
The unit of reuse is component + its subs + its events. Never store entity state in component-local atoms away from app-db. (Local ratoms are fine for transient UI state like an uncommitted input draft.)
Controlled inputs (laggy typing)
A controlled :input wired subscribe + dispatch-on-change drops characters during fast typing (async dispatch vs animation frames). Fixes: dispatch-sync in on-change, commit on blur, or keep a local ratom draft and dispatch on "apply":
(defn coupon-panel []
(let [draft (reagent/atom "")] ;; form-2: draft survives re-renders
(fn []
(let [coupon @(rf/subscribe [:cart/coupon])] ;; deref INSIDE inner fn
[:div
[:input {:value @draft :on-change #(reset! draft (-> % .-target .-value))}]
[:button {:on-click #(rf/dispatch [:cart/apply-coupon @draft])} "Apply"]]))))
DOM events
Don't dispatch js event objects (React pools them; re-frame handles async). Extract the salient data and dispatch pure CLJS data expressing intent.
CPU-heavy work
All CPU-heavy work lives in event handlers (never subs/views). Chunk to <100ms (~16ms ideal) and redispatch to self, carrying progress in the event/db; each dispatch yields the thread so the browser can paint. Set a "working" flag in db for UI; cancel via an abandonment flag checked at each chunk. To force a paint before a one-shot hog: :dispatch ^:flush-dom [:heavy/work]. Classic bug: (do (assoc db ...) (long-computation)) in a -db handler discards the assoc — the handler must return the new db.
Timers / polling
Model start/stop as data handled by one effect holding an atom of live intervals:
{:interval {:action :start :id :poll-cart :frequency 60000 :event [:cart/fetch]}}
{:interval {:action :cancel :id :poll-cart}}
Use defonce for the atom and a :clean action for hot-reload safety.
Focus
(rf/reg-fx :focus-to-element
(fn [id] (reagent.core/after-render
#(some-> js/document (.getElementById id) .focus))))
;; after-render: the element may not exist until the next frame
Debugging setup
- Tools: re-frame-10x (or re-frisk) + cljs-devtools. shadow-cljs dev build:
:compiler-options {:closure-defines {re-frame.trace.trace-enabled? true
goog.DEBUG true}}
:devtools {:preloads [day8.re-frame-10x.preload]}
- Inspect app-db at REPL:
@re-frame.db/app-db. debuginterceptor logs event + db diff; include as(when ^boolean goog.DEBUG rf/debug).- Loggers:
(rf/set-loggers! {:warn my-warn ...}).
Instrumented namespaces (re-frame 1.4.7+)
re-frame.core-instrumented (and re-frame.alpha-instrumented) are drop-in mirrors where dispatch/subscribe/reg-* are macros attaching call-site {:file :line} metadata (elided when goog.DEBUG is false). Answers "who dispatched this / where was this registered" in tooling. Caveat: macros can't be used in value position — keep a second require of plain re-frame.core for (apply reg-sub ...), (map reg-event-db ...), etc.:
(:require [re-frame.core-instrumented :as rf]
[re-frame.core :as rf-core]) ;; for function-position uses
Known-good versions
re-frame 1.4.7, re-frame-10x 1.12.3, re-com 2.29.3, day8.re-frame/tracing 0.9.2. Scaffold: lein new re-frame your-app +10x or pitch-io/uix-starter --re-frame.