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 via dispatch. 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.
  • debug interceptor 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.