02 — Views¶
Views are where the cascade ends and pixels begin. The view layer in re-frame2 is substrate-agnostic — the same reg-view registration works against Reagent, UIx, and Helix; the same frame-handle and frame-bound-fn helpers compose across all three. The substrates differ in how they emit React calls; the re-frame2 contract sits above that and stays uniform.
This chapter covers the registration surface (reg-view, reg-view*, view), the substrate-agnostic ergonomic surface (frame-handle, frame-bound-fn / frame-bound-fn*, with-frame, with-new-frame, frame-provider), and points at the per-adapter chapters for the substrate-specific hooks. If you want the Reagent vs UIx vs Helix conventions, see 13 — Lifecycle and 14 — Adapters.
The view registry¶
The view registry is what [my-view "arg"] resolves against. Every view you register lives in it; every view that emits hiccup ends up rendered through it. The registry is keyed by id (a keyword, conventionally (keyword *ns* sym) so the view's id matches its symbol).
reg-view¶
- Kind: macro
- Signature:
- Description: The defn-shape view registration. Auto-defs the symbol, auto-derives the id from
(keyword *ns* sym), auto-injectsdispatch/subscribeas lexical bindings, and rejects non-defn-shape bodies at macroexpand. The 80% of registrations want this form. - Example:
- In the wild: counter
reg-view*¶
- Kind: function
- Signature:
- Description: The plain-fn surface beneath
reg-view. No auto-def (the caller manages the Var or computed id), no auto-inject, no compile check. Reach for it when you need: computed ids, library-generated views, Reagent Form-3 (create-class), or registration without a Var.
view¶
- Kind: function
- Signature:
- Description: Runtime lookup handle. Returns the registered render-fn, not hiccup. Use in hiccup as
[(rf/view :id) args...]when you need to late-bind a view by id (computed id, plugin-style dispatch, dynamic chrome).
How reg-view reads¶
The macro accepts three shapes for the same registration. They produce the same registered view; the choice is about what you want at the source level.
;; Bare form — id derived as :my.app.cart/cart-line
(rf/reg-view cart-line [item]
[:tr
[:td (:name item)]
[:td (:qty item)]])
;; With docstring — useful for the registry's :doc field
(rf/reg-view cart-line
"One row in the cart table; receives a normalised item map."
[item]
[:tr ...])
;; With explicit id via metadata — useful when the symbol shouldn't drive the id
;; (e.g. you want a stable id across rename, or you're matching an external contract).
(rf/reg-view ^{:rf/id :cart/line} cart-line [item]
[:tr ...])
In all three cases the symbol cart-line is def-ed so you can write [cart-line item] from sibling code. The macro also injects dispatch and subscribe as lexical bindings so you can call them without the rf/ prefix inside the body — this matters less than it used to (the rf/ prefix is conventional) but the seam is preserved for muscle-memory and macro composition.
When to reach for reg-view*¶
The starred form is the right call when:
- The id is computed. Code-gen pipelines, plugin systems, story / variant scaffolding — anywhere the id isn't a literal symbol at the call site.
- You don't want a Var. Inside a
letor a closure, or when the view is a one-off built from configuration data. - You're writing a Form-3 component. Reagent's
create-classwraps a map of lifecycle methods around a render-fn; the call shape is(rf/reg-view* :id (r/create-class {...})). - You're consumer-side library code. Libraries that ship registered views (a charting library, a table widget) often want to register without imposing a
defon the consumer's namespace.
The * follows Clojure's own let / let*, fn / fn* idiom — the un-starred form is the macro shorthand; the starred form is the underlying primitive.
The view lookup form¶
Two shapes coexist in hiccup:
- Var form
[my-view arg1 arg2]— the canonical, source-readable form. Reagent / UIx / Helix all resolve a function-in-tag-position by calling it with the trailing args. - Lookup form
[(rf/view :my/id) arg1 arg2]— for late-binding by id. Same render outcome; different addressing scheme.
Bare keyword-tagged hiccup ([:my-view "args"]) is removed in v2 — it was a v1 footgun that collided with HTML tag keywords. Use the Var form or the lookup form.
The substrate-agnostic ergonomic surface¶
These surfaces work the same across Reagent, UIx, and Helix. They're how views interact with the running app without being tied to any single substrate's idiom. They sort into three intents: scope (frame-provider, with-frame / with-new-frame), hold (frame-handle, frame-bound-fn / frame-bound-fn*), and override (the {:frame …} opt, rowed in 01 — Core). The full design lives at Spec 002 §The multi-frame surface.
frame-provider¶
- Kind: Reagent component
- Signature:
- Description: "Children inside this provider see
:todoas their current frame." Lexical scope for the implicit frame; nestable; pairs withwith-framefor non-component code.
with-frame / with-new-frame¶
- Kind: macros (sibling pair)
- Signatures:
- Description:
with-framepins*current-frame*to an existing frame-id; the frame is not created or destroyed.with-new-frameevalsexpr, binds the resulting id tosym, runs body in that frame's dynamic context, and destroys the frame on exit. Each rejects the other's argument shape at compile time. Documented in 002 §with-frame and with-new-frame.
frame-handle¶
- Kind: function
- Signature:
- Description: The keystone affordance. Captures the active frame at CREATION time and returns an operation bundle whose
:dispatch/:dispatch-sync/:subscribeops always target the captured frame — they survive async boundaries (Promise.then,setTimeout, WebSocketonmessage, observer callbacks) where the ambient frame lookup would have unwound. The handle is locked to one frame: a per-call:frameopt MUST NOT override it. It's an operation bundle, not a container — read the frame's app-db value via(rf/app-db-value (:frame handle)), not the handle itself. - Example:
frame-bound-fn¶
- Kind: macro
- Signature:
- Description: The
fn-syntax twin offrame-handlefor the case where the value you carry across the boundary isn't a dispatch/subscribe op but an arbitrary fn whose body re-establishes the frame (an async result handler, an interval handle, a fn that callscurrent-frame-idinternally). Captures the active frame at the lexical-binding moment; when the returned fn is later invoked, it runs in abinding [*current-frame* <captured-frame>]block, so plaindispatch/subscribeinside the body pick up the right frame regardless of when the call fires. CLJS-only macro. - Example:
frame-bound-fn*¶
- Kind: function
- Signature:
- Description: The
*-twin offrame-bound-fn— wraps an existing fn value (or one returned by another helper / library) rather than takingfn-syntax. The 2-arity form takes an explicitframe-id, so no surroundingwith-frame/frame-provideris needed at wrap time — useful at module top level, ininstall!routines, and in module helpers. - Example:
When to reach for frame-handle / frame-bound-fn¶
The verbs dispatch and subscribe read the current frame ambiently (dynamic var → React context) at call time. That's fine when the call sits inside an established scope — inside a render, an event handler, a sub computation, a with-frame block. It breaks when the call sits outside that scope — a Promise callback, a setTimeout, a WebSocket onmessage, an IntersectionObserver. By the time the callback fires, the ambient scope has unwound, the token carries no frame stamp, and a bare (rf/dispatch [::foo]) fails loudly with :rf.error/no-frame-context (per EP-0002, the runtime never synthesises :rf/default from absence — frame identity is carried, not found).
The fix is to capture the frame at the point you have it and carry it as a value:
frame-handle— the common case. You need a:dispatch/:subscribeop to hand to a callback or an async library. Build it inside a render body or underwith-frame; store the bundle; invoke its ops from any later async context.frame-bound-fn/frame-bound-fn*— when the value crossing the boundary is an arbitrary fn whose body must re-establish the frame (so plaindispatch/subscribeinside that fn resolve correctly), not a single dispatch/subscribe op. The macro takesfn-syntax; the*-twin wraps an existing fn and accepts an explicitframe-id.
;; frame-handle composes inside with-frame
(rf/with-frame :tool
(let [{:keys [dispatch]} (rf/frame-handle)] ;; captures :tool frame
(js/setTimeout #(dispatch [::tick]) 1000))) ;; fires :tool even after with-frame unwinds
Full async-boundary contract (the four routing patterns and the React click-handler case): Spec 002 §React click-handler routing.
with-frame and with-new-frame — the sibling pair¶
;; Pin form — most common: bind *current-frame* to an existing id
(rf/with-frame :todo
(rf/dispatch [::add-item ...]))
;; Pin to a computed id — pass the keyword directly, no extra binding
(let [chosen (compute-frame-id ...)]
(rf/with-frame chosen
(rf/dispatch [::action chosen])))
;; Eval-bind-run-destroy form — create a throwaway frame for the body
(rf/with-new-frame [f (rf/make-frame {:on-create [:test/initialise]})]
(rf/dispatch [::action f])) ;; frame destroyed on exit
Full semantics in 002 §with-frame and with-new-frame.
Reagent: the default substrate¶
The CLJS reference implementation ships against Reagent as the default substrate. There's no separate re-frame.adapter.reagent namespace to require — re-frame.core includes the Reagent adapter inline, because that's the historical default and the path of least surprise for re-frame v1 migrators.
Reagent views are plain Clojure functions returning hiccup; re-frame2's reg-view macro is the typed sugar over defn + reg-view*. Form-2 (a fn that returns a fn) and Form-3 (create-class) are both supported; the wrapping the macro emits is transparent to either pattern.
The adapter spec map — the value (rf/init!) consumes — lives at re-frame.adapter.reagent/adapter (Reagent-full) or re-frame.adapter.reagent-slim/adapter (Reagent without the React server-rendering tax, for SSR pipelines).
UIx and Helix: hooks-shaped substrates¶
UIx and Helix expose React's hooks model directly. The re-frame2 adapter for each ships in its own artefact (day8/re-frame2-uix, day8/re-frame2-helix) and exposes a small, parallel surface — the same shape across both, because the Helix decisions transfer the UIx decisions one-for-one.
In the entries below, <adapter> stands for the adapter namespace alias the consumer chose at require — typically uix-adapter or helix-adapter.
<adapter>/adapter¶
- Kind: Var (map)
- Signature:
- Description: The adapter spec passed to
(rf/init! ...). - Example:
- In the wild: counter_uix · counter_helix
<adapter>/use-subscribe¶
- Kind: hook (function)
- Signature:
- Description: The hook-shaped read. Matches the React/UIx/Helix idiom; there's no auto-injection — components call the hook and
(rf/frame-handle)directly. - Example:
- In the wild: counter_uix · counter_helix
<adapter>/use-current-frame¶
- Kind: hook (function)
- Signature:
- Description: "What frame am I in?" — for components that need to thread the frame through hand-written child callbacks.
<adapter>/frame-provider¶
- Kind: component (function)
- Signature:
- Description: The component-shaped equivalent of Reagent's
frame-provider. The underlying React Context (re-frame.adapter.context) is shared across all three substrates, so a mixed-substrate app's frame-provider chain composes across substrate boundaries.
<adapter>/wrap-view¶
- Kind: function
- Signature:
- Description: Adapter-side source-coord annotation. Most UIx / Helix users register through
reg-view*and let the adapter wrap;wrap-viewis exposed for code-gen and library scaffolding.
<adapter>/flush-views!¶
- Kind: function
- Signature:
- Description: Wraps React's
act()for tests. Drain queued state updates before assertions.
<adapter>/set-hiccup-emitter!¶
- Kind: function
- Signature:
- Description: Install a render-tree → HTML fn. Parity with the Reagent adapter's late-bind seam for SSR.
The UIx / Helix adapters do not support reg-view (the macro is Reagent-specific in its defn-shape rewriting). UIx and Helix users register with rf/reg-view* when they need registry-keyed view addressing — most don't, because UIx and Helix components compose by Var reference like ordinary React components.
See 14 — Adapters for the per-substrate detail.
DOM source-coord annotations¶
Every adapter whose host has a DOM-attribute concept (Reagent / UIx / Helix on the browser; not Plain Atom) injects data-rf2-source-coord="<ns>:<sym>:<line>:<col>" on the rendered root DOM element of each registered view. The annotation is mandatory at the adapter contract level; it's what powers click-to-source navigation in Xray and re-frame2-pair.
Annotation is gated on interop/debug-enabled? (the CLJS mirror of goog.DEBUG). Production :advanced builds elide the attribute via dead-code elimination — there's no DOM-bytes cost in shipped bundles.
The JVM SSR emitter mirrors the same contract so server-rendered HTML can be clicked back to the source position before any hydration happens.
Full contract: Spec 006 §Source-coord annotation and Spec 011 §Source-coord annotation under SSR.
See also¶
- 01 — Core —
dispatch,subscribe,reg-view, and the{:frame …}override opt rowed in registration. - 03 — Effects and interceptors —
with-fx-overridesfor scoping fx behaviour inside a view's event handlers. - 14 — Adapters — full per-substrate surface tables.