02 — Views¶
Views are where the cascade ends and pixels begin. The view layer in re-frame2 is substrate-agnostic — the shared dataflow (frames, subscriptions, dispatch, source metadata, registry ids) is uniform across Reagent, UIx, and Helix, and the same frame-handle carry primitive composes across all three. The substrates differ in how they emit React calls; the re-frame2 contract sits above that and stays uniform.
There are two registration lanes, and you almost certainly want only one of them:
- App-facing view registration — what application authors use to register and render their own views. This is the
reg-viewmacro (Reagent) plus rendering by Var reference. Read this if you are building screens. - Tooling / host view registration — what tools, dynamic hosts, and library code use to register views from computed ids and render them by id at runtime. This is
reg-view*plus(rf/view id). Read this only if you are embedding views you don't write at the call site — tool panels, story canvases, code-gen pipelines, plugin systems.
If you're not sure, you're an application author: use the app-facing lane and skip the tooling lane entirely.
This chapter also covers the substrate-agnostic ergonomic surface (frame-handle, 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.
App-facing view registration¶
This is the lane for application authors. You register a view with reg-view, render it by Var reference, and that's the whole story for the 80% case. The view registry that backs this is what [my-view "arg"] resolves against: every view you register lives in it, 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
There is one app-facing exception that still lives in the macro family: Reagent Form-3 components (reagent.core/create-class) aren't defn-shaped, so the reg-view macro can't wrap them. Those register through reg-view* — see Tooling / host view registration below, where the rest of the starred-form callers live.
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.
Rendering an app-facing view¶
In the app-facing lane you render a view by Var reference — the symbol reg-view def-ed:
Reagent / UIx / Helix all resolve a function-in-tag-position by calling it with the trailing args. 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. The by-id lookup form [(rf/view :id) args] exists too, but it belongs to the tooling lane below — application authors reaching for it usually want a plain Var reference instead.
Tooling / host view registration¶
This is the lane for tools, dynamic hosts, and library code — not application screens. Reach for it when the thing rendering a view doesn't know the view at the call site: a tool panel hosting an arbitrary registered view, a story canvas that stores a view id in data, a code-gen pipeline that emits views from a manifest, or a plugin system that late-binds by id. It is built from two surfaces: reg-view* (register from a computed id or a non-defn render fn) and view (resolve a registered render-fn by id at runtime). If you're writing application views, you don't need either — use reg-view and Var references above.
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. 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 {...})). This is the one app-facing reason to touch the starred form. - 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.
- Note: The
*follows Clojure's ownlet/let*,fn/fn*idiom — the un-starred form is the macro shorthand; the starred form is the underlying primitive. Inside areg-view*body there's no auto-injecteddispatch/subscribe; capture a(rf/frame-handle)at render and use its ops if the view needs frame-bound dispatch.
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 — a host that resolves a stored view id, plugin-style dispatch, dynamic chrome. This is how tool shells host an arbitrary registered view: they read a view id out of data, resolve it with(rf/view id), and render the result. - Example:
The lookup form and the Var form produce the same render outcome — they differ only in addressing scheme. The Var form ([my-view args]) is the app-facing default; the lookup form ([(rf/view :id) args]) is for the tooling/host case where the id, not the symbol, is what the caller holds.
Keep internal tool panels out of app-facing docs¶
A tool's own chrome — Story's panel grid, Xray's inspector views — is registered with reg-view* and hosted by id, but those panel components are internal to the tool, not part of the application author's view surface. When documenting an application, leave them out: an app author registers their screens with reg-view and never sees the host's panel registry. The tooling lane exists so that tools can host the app's views, not so that app authors adopt the tool's internals.
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-existing, with-frame), own (frame-provider, with-new-frame), hold (frame-handle — the one public carry primitive), 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 (UI-owned lifecycle boundary)
- Signature:
- Description: The view-owned lifecycle boundary — it creates the frame on mount (via
make-frame, taking the same constructor opts::id/:images/:initial-db/ record-config), provides its id to descendants, and destroys it on unmount. Reach for it when a component should own a frame for exactly as long as it is mounted (comparison pages, Story canvases, embedded widgets, modal stacks). To merely scope an already-created frame, useframe-provider-existing(below).
frame-provider-existing¶
- Kind: Reagent component (scope-only)
- Signature:
- Description: "Children inside this provider see
:todoas their current frame." Scopes a React subtree to a frame that already exists (created bymake-frame/reg-frame, a tool runtime, or an enclosingframe-provider); creates / refreshes / destroys nothing. Takes:frameonly — a lifecycle opt fails loud. The scope-into-React counterpart towith-frame(which a dynamic var cannot serve across React's render boundary).
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/frame-bound-fn*are internal under EP-0024. Earlier re-frame2 also shippedframe-bound-fn(afn-syntax macro) andframe-bound-fn*(its*-twin) for wrapping an arbitrary fn whose body re-establishes the frame. EP-0024 Open Issue #8 retiered both to internal (:tier :implementation) —frame-handle(or an explicit{:frame …}opt) expresses the real use cases, and the empirical backbone found no app or tool calling them. They are no longer app API; author async / tooling paths withframe-handle.
When to reach for frame-handle¶
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 with frame-handle: build the operation bundle inside a render body or under with-frame, store it, and invoke its :dispatch / :subscribe ops from any later async context.
;; 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 {:images [test-image]})]
(rf/dispatch-sync [:test/initialise]) ;; seed via a setup dispatch
(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 / <adapter>/frame-provider-existing¶
- Kind: components (functions)
- Signatures:
- Description: The component-shaped equivalents of Reagent's
frame-provider(UI-owned lifecycle) andframe-provider-existing(scope-only). The underlying React Context (re-frame.adapter.context) is shared across all three substrates, so a mixed-substrate app's 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). For UIx and Helix the app-facing lane is native components plus the adapter hooks — most application views need no view registration at all, because UIx and Helix components compose by Var reference like ordinary React components. reg-view* is the tooling/host lane here too: reach for it only when something needs registry-keyed view addressing (a tool hosting the view by id, a code-gen pipeline).
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.