Subscriptions and the Signal Graph
app-db is the root of a directed acyclic Signal Graph; view functions are the leaves; reg-sub creates the interior nodes. Nodes exist only while some rendered view needs them (created lazily, destroyed when unused). Two concurrent subscribes with = query vectors share one node — expensive computations run once for all consumers.
The four layers
- Ground truth —
app-db. - Extractors — subs reading directly from app-db. Rerun on every db change, so they must be computationally trivial (pluck a fragment). They are
=circuit-breakers: if the fragment is unchanged, downstream never reruns. - Materialised views — subs whose inputs are other subs (never app-db). All real computation (sort/filter/join/aggregate) goes here; only reruns when its input signals actually change.
- Views — subscribe to layer 2/3, produce hiccup only.
A node reruns when inputs change but propagates only if its output differs (=) from last time.
reg-sub forms
Layer 2 — input defaults to app-db:
(reg-sub
:cart/items
(fn [db query-v] ;; query-v = full vector passed to subscribe
(get-in db [:cart :items])))
Layer 3 — canonical form with explicit signals fn:
(reg-sub
:cart/summary
(fn [query-v] ;; signals fn: one signal, vector, or map of signals
[(subscribe [:cart/items]) (subscribe [:cart/coupon])])
(fn [[items coupon] query-v] ;; computation fn: inputs first
(calculate items coupon)))
:<- sugar (one pair per input signal):
(reg-sub
:cart/summary
:<- [:cart/items]
:<- [:cart/coupon]
(fn [[items coupon] _] (calculate items coupon)))
;; single input — no vector destructuring:
(reg-sub
:cart/item-count
:<- [:cart/items]
(fn [items _] (count items)))
Computation-fn sugar
:-> f— computation fn is 1-arity, receiving only input values (query vector omitted). Ideal for keyword/path accessors:(reg-sub :auth/token :-> #(get-in % [:auth :token])). Never pass a bare keyword as the 2-arity computation fn — a keyword called with 2 args returns the second arg (the query vector) when the key is missing;:->avoids this trap.:=> f— computation fn called as(f input-values & query-args), spreading the query vector's tail as args:(reg-sub :cart/item :<- [:cart/items] :=> (fn [items id] (get items id)))for(subscribe [:cart/item 42]).
Parameterised subs: the whole query vector reaches the handler — (subscribe [:cart/items-by-colour "blue"]), (fn [db [_ colour]] ...).
Rules
- Extract computation fns as named
defns (in.cljc) and register them — directly testable:
(defn visible-todos [[todos showing] _] ...)
(reg-sub :todos/visible :<- [:todos/all] :<- [:todos/showing] visible-todos)
- No generic path subscription (
(reg-sub :get (fn [db [_ path]] (get-in db path)))) — views become coupled to db structure; restructuring app-db breaks every view. Per-key extractors are the right kind of repetition. - Never subscribe outside a render fn (event handlers, callbacks) — leaks (only render fns give Reagent a disposal hook).
re-frame.alpha'ssubis memory-safe anywhere, at recomputation cost. - Classic no-rerender bug: handler writes to a path different from the one layer 2 reads → layer 2 output
=unchanged → layer 3 cache hit → view never updates. When a view "doesn't update", walk the layer 2 → 3 chain comparing pre/post values to find which equality gate held. - Form-2 gotcha:
(let [v @(subscribe ...)] (fn [] v))freezes the value. Keep the subscription in the outer let and deref in the inner fn:(let [v* (subscribe ...)] (fn [] @v*)). defmultiviews: adefmethodreturning a fn silently becomes form-2 and Reagent caches it forever — keep all methods form-1, or usecase/cond.
Performance
- Reagent decides re-render via
=on props: give each child only the data it needs (don't pass the whole app state to 300 children); split big components so rows subscribe to row data. - Use
^{:key ...}on list items. - Inline
(fn [e] ...)handlers are new each render; if a component produces lots of DOM, lift handlers into a form-2 outer let. Don't over-optimize otherwise. - Diagnose with re-frame-10x tracing.