Skip to content

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:
    (reg-view sym [args] body+)
    (reg-view sym docstring [args] body+)
    (reg-view ^{:rf/id :explicit/id} sym [args] body+)
    
  • Description: The defn-shape view registration. Auto-defs the symbol, auto-derives the id from (keyword *ns* sym), auto-injects dispatch / subscribe as lexical bindings, and rejects non-defn-shape bodies at macroexpand. The 80% of registrations want this form.
  • Example:
    (rf/reg-view counter-buttons []
      [:div
       [:button {:on-click #(dispatch [:counter/dec])} "-"]
       [:span @(subscribe [:counter/value])]
       [:button {:on-click #(dispatch [:counter/inc])} "+"]])
    
  • In the wild: counter

reg-view*

  • Kind: function
  • Signature:
    (reg-view* id render-fn)
    (reg-view* id metadata render-fn)
    
  • 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:
    (view view-id)  render-fn
    
  • 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 let or a closure, or when the view is a one-off built from configuration data.
  • You're writing a Form-3 component. Reagent's create-class wraps 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 def on 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

[(rf/view :app/header) {:title "Cart"}]   ;; resolves the registered render-fn at render time

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:
    [rf/frame-provider {:frame :todo} & children]
    
  • Description: "Children inside this provider see :todo as their current frame." Lexical scope for the implicit frame; nestable; pairs with with-frame for non-component code.

with-frame / with-new-frame

  • Kind: macros (sibling pair)
  • Signatures:
    (with-frame :keyword body)        ;; pin to an existing frame-id
    (with-new-frame [sym expr] body)  ;; eval, bind, run, destroy on exit
    
  • Description: with-frame pins *current-frame* to an existing frame-id; the frame is not created or destroyed. with-new-frame evals expr, binds the resulting id to sym, 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:
    (frame-handle)           {:frame :dispatch :dispatch-sync :subscribe}
    (frame-handle frame-id)  {:frame :dispatch :dispatch-sync :subscribe}
    
  • Description: The keystone affordance. Captures the active frame at CREATION time and returns an operation bundle whose :dispatch / :dispatch-sync / :subscribe ops always target the captured frame — they survive async boundaries (Promise.then, setTimeout, WebSocket onmessage, observer callbacks) where the ambient frame lookup would have unwound. The handle is locked to one frame: a per-call :frame opt 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:
    (rf/reg-view stream-view []
      (let [{:keys [dispatch]} (rf/frame-handle)]          ;; captures the render frame
        (ws/subscribe! (fn [msg] (dispatch [::incoming msg]))) ;; fires LATER, but bound
        [:div "streaming…"]))
    

frame-bound-fn

  • Kind: macro
  • Signature:
    (frame-bound-fn [args] body)
    
  • Description: The fn-syntax twin of frame-handle for 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 calls current-frame-id internally). Captures the active frame at the lexical-binding moment; when the returned fn is later invoked, it runs in a binding [*current-frame* <captured-frame>] block, so plain dispatch / subscribe inside the body pick up the right frame regardless of when the call fires. CLJS-only macro.
  • Example:
    (rf/reg-view alert-widget []
      [:button {:on-click (rf/frame-bound-fn [_]                ;; captures NOW
                            (.then (fetch "/notify")
                                   (fn [_] (rf/dispatch [::notified]))))} ;; runs LATER, but bound
       "Notify"])
    

frame-bound-fn*

  • Kind: function
  • Signature:
    (frame-bound-fn* f)           frame-bound fn
    (frame-bound-fn* frame-id f)  frame-bound fn
    
  • Description: The *-twin of frame-bound-fn — wraps an existing fn value (or one returned by another helper / library) rather than taking fn-syntax. The 2-arity form takes an explicit frame-id, so no surrounding with-frame / frame-provider is needed at wrap time — useful at module top level, in install! routines, and in module helpers.
  • Example:
    ;; wrap an existing fn — captures the ambient frame
    (rf/frame-bound-fn* (fn [msg] (rf/dispatch [::incoming msg])))
    
    ;; wrap with an explicit frame-id — no ambient frame needed
    (rf/frame-bound-fn* :rf/xray
      (fn [_e mode] (rf/dispatch [::set-mode mode])))
    

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 / :subscribe op to hand to a callback or an async library. Build it inside a render body or under with-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 plain dispatch / subscribe inside that fn resolve correctly), not a single dispatch/subscribe op. The macro takes fn-syntax; the *-twin wraps an existing fn and accepts an explicit frame-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).

(:require [re-frame.adapter.reagent :as reagent])

(rf/init! reagent/adapter)

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:
    {:make-state-container 
     :render 
     :dispose-adapter! }
    
  • Description: The adapter spec passed to (rf/init! ...).
  • Example:
    (:require [re-frame.adapter.uix :as uix-adapter])
    
    (rf/init! uix-adapter/adapter)
    
  • In the wild: counter_uix · counter_helix

<adapter>/use-subscribe

  • Kind: hook (function)
  • Signature:
    (use-subscribe query-v)  value
    (use-subscribe frame-kw query-v)  value
    
  • 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:
    (let [count    (uix-adapter/use-subscribe [:count])
          {:keys [dispatch]} (rf/frame-handle)]
      ($ :button {:on-click #(dispatch [:inc])} count))
    
  • In the wild: counter_uix · counter_helix

<adapter>/use-current-frame

  • Kind: hook (function)
  • Signature:
    (use-current-frame)  frame-kw
    
  • 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:
    ($ frame-provider {:frame :session :children []})
    
  • 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:
    (wrap-view id metadata user-fn)  wrapped fn
    
  • Description: Adapter-side source-coord annotation. Most UIx / Helix users register through reg-view* and let the adapter wrap; wrap-view is exposed for code-gen and library scaffolding.

<adapter>/flush-views!

  • Kind: function
  • Signature:
    (flush-views!)
    (flush-views! f)
    
  • Description: Wraps React's act() for tests. Drain queued state updates before assertions.

<adapter>/set-hiccup-emitter!

  • Kind: function
  • Signature:
    (set-hiccup-emitter! f)
    
  • 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