re-frame.ui¶
Donor code — re-frame.ui is not published, and there is no coordinate to add
day8/re-frame2-ui is not a Maven coordinate and never will be. The
compiled-view substrate is donor-only code being absorbed into
Freehand; the standalone artefact is deleted at
the EP-0036 F6e gate. The release process is
normative on that, and the artefact is absent from the release workflow's
deploy matrix. There is nothing here you can put in a deps.edn.
This page is retained as the contract record for view code an app already
carries on the substrate — the audience of the
re-frame2-ui skill. New view work starts at
Freehand and its
install page.
re-frame.ui is the compiled-view substrate donated to the Freehand programme.
Views are authored as defview + hiccup; the compiler lowers templates to direct
React construction (browser) and a versioned structural tree (JVM). There is no
hiccup interpreter in production bundles on the compiled path.
This namespace is not re-exported from re-frame.core. Donor code requires it
explicitly and installs its adapter at boot:
Concept teaching for the substrate lives in the
re-frame2-ui skill and the substrate design suite;
the donor guide chapters were replaced by the
Freehand guide. This page is the contract surface for
public symbols.
Stage honesty. Front-porch symbols below are the ruled public API. Some runtime behaviours (committed DOM handlers and hydration manifests) land with later substrate stages; the names and signatures are the v1 surface. Where a direct call is only a compile-time resolution symbol, the description says so.
Boot¶
adapter¶
- Kind: Var (adapter map)
- Signature: pass to
(rf/init! ui/adapter) - Description: The
re-frame.uisubstrate adapter. CLJS installs the native React observation substrate (no Reagent / UIx). JVM uses the headless atom realisation of the same closed adapter contract. Discriminator:rf.adapter/ui. Destroying the adapter tears down every public compiled Root, then the generic React spine. The JavaScript host must provideWeakRef; Root admission probes/captures it once before any React/registry/ViewCell ownership mutation and otherwise throws:rf.error/ui-platform-incompatiblewith:platform :javascript,:capability :js/WeakRef, and recovery:use-a-weakref-capable-javascript-runtime.FinalizationRegistryis optional; synchronous WeakRef compaction covers hosts without it. There is no strong fallback, polling, or per-render capability check. - Example:
Views¶
defview¶
- Kind: macro
- Signature:
- Description: The one component form. A pure function of zero or one props map to a hiccup template, compiled to direct React (browser) and the JVM structural tree. Header destructuring lowers to per-slot reads. Closed options map:
:props— Malli schema (dev-checked; compile-time when the call site is literal):id— registry override:display-name— React display name Every view is memoised on a per-slotrf=comparator and registers under the registrar's:viewkind. No Form-2 / Form-3 / positional args.- Example:
custom-element¶
- Kind: macro
- Signature:
- Description: Declares a custom-element tag and which kebab-case prop names are JS properties (camelCased on the client). Undeclared names are attributes; undeclared elements need no declaration (all-attributes default). JVM emits attributes only; property props apply at hydration.
- Example:
Template forms (compiler-owned)¶
These symbols resolve in templates so the compiler can lower them. A direct call from ordinary Clojure code fails loud (they are not runtime helpers).
sub¶
- Kind: function (compile-time authoring form)
- Signature:
(sub query-vector) → value(inside a compiled view only) - Description: Lexical subscription read site. Returns the current value; the view
re-renders when it changes. Conditional sites are legal; sites inside
forare a compile error (extract a keyed child view). Not a one-shot helper — use coresubscribe/subscribe-onceoutside views. - Errors: direct call →
:rf.error/ui-tree-malformed.
frame¶
- Kind: function (compile-time authoring form)
- Signature:
(frame) → {:frame :dispatch :dispatch-sync :subscribe} - Description: Inside a compiled view, returns the capture-frame-shaped ops bundle
bound to the committed frame. Ops fail loud after the frame incarnation dies
(
:rf.error/frame-destroyed). Outside views userf/capture-frame.
local¶
- Kind: function (compile-time authoring form)
- Signature:
(local init) → [value set! update!](bound in adefviewtop-regionlet) - Description: Host component-local ephemera, deliberately outside re-frame2
epochs — not observed by subs, not revertible by epoch restore, re-renders this
view only. The P0-1 three-tuple:
set!stores its argument exactly (a stored function is a value, never an updater — nouseStatefn-overload);update!applies(f current & args)to the latest host state so several same-turn writers (key + pointer + timer + observer) compose instead of last-write-wins. Setter/updater are host-only — a mutation during render fails loud; committed same-view handlers andeffectcallbacks may read/update. On the JVM structural renderlocalexposes the initial value;set!/update!raise:rf.error/jvm-host-op. - Placement: legal only in a
defview's unconditional top region (not a loop, branch, deferred callback, or render-fn slot →:rf.ui.compile/hook-misplaced).
ref¶
- Kind: function (compile-time authoring form)
- Signature:
(ref) / (ref init) → host ref object(bound in adefviewtop-regionlet) - Description: The substrate-native DOM-node ref — the everyday "I need the DOM
node" primitive (focus, measurement, a third-party widget), promoted from the
re-frame.ui.reactref hook (rf2-u53yy.9). Bound in the top-regionletand handed to a:refposition; read/write via(.-current node). Assignment never re-renders — its reason to exist besidelocal— and it is the preferred object ref for a:refslot; no deps. The object ref attaches at commit before effects fire, so(.-current node)is populated by the firsteffecttime (guard it withwhen-letand include the ref in the effect's deps). Callback refs via(ui/raw-fn f)remain the expert seam for when the node's identity change must itself trigger work. On the JVM structural render it is an inert ref (currentnil, stays nil); refs never appear in the JVM tree. - Placement: a host hook — legal only in a
defview's unconditional top region (not a loop, branch, deferred callback, or render-fn slot →:rf.ui.compile/react-hook-misplaced); contributes its:refkind to the view's hook signature.
effect¶
- Kind: function (compile-time authoring form)
- Signature:
(effect [deps…] body…)/(effect :connect body…)(a leading statement) - Description: Passive host effect for synchronising with the world outside the tree
(measurement, chart/animation libraries via a ref). The value-deps form re-runs its
body after commit whenever the literal deps change, compared by
rf=; a returned function is the cleanup honoured on dep-change, disconnect, and unmount.:connectruns at each connect (mount / Activity reveal) with cleanup at each disconnect — there is deliberately no "once"/"mount" name. StrictMode dev replay is expected and must be idempotent-safe (that is what cleanup is for).sub/frameinside an effect body are compile errors. On the JVM effects do not run (capability metadata only). - Placement: a leading statement in a
defviewtop region (or a top-regionlet/dobody) before the template (:rf.ui.compile/hook-misplacedotherwise).
dispatch-fn¶
- Kind: function (compile-time authoring form)
- Signature:
(dispatch-fn) → (fn ([event] [event opts])) - Description: The per-view stable committed-frame dispatcher for imperative /
foreign callbacks. Its identity is stable across renders (attach it as a listener
once); it reads the committed frame at call time and retargets only on commit; and
it fails loud in every non-connected state (
:rf.error/dispatch-disconnected) — the leaked-listener detector for a callback that outlived its view. Capture it in the view body and use it from aneffectcallback or a foreign event bridge. On the JVM a dispatch invocation raises:rf.error/jvm-host-op.
raw¶
- Kind: function (template interop form)
- Signature:
(raw react-element) - Description: Embed an existing React element in child position. On the JVM a
rendered
rawchild is a host-op error.
html¶
- Kind: function (template interop form)
- Signature:
(html string) - Description: Trusted markup bypass — the sole child of a DOM element
(
[:div (ui/html s)]). Visible at the call site; both emitters treat it identically.
raw-fn¶
- Kind: function
- Signature:
(raw-fn f) → f - Description: Identity-as-protocol callback marker (also the explicit callback-ref form). Passes the function through to the host untouched.
spread¶
- Kind: function (template interop form)
- Signature:
(spread base overrides)(DOM element) ·(spread literal-part runtime-map)or(spread runtime-map)(foreign component) - Description: The one runtime prop-map form. Its behaviour depends on the head:
- DOM / custom element —
(ui/spread base overrides), e.g.[:div (ui/spread base attrs)]: two runtime maps merged through the same conversion rule table as the compiler (later-arg-wins). - Foreign component —
[DatePicker (ui/spread {…} forwarded)]: the standard wrapper idiom (accept a map, forward it onto a foreign component). The optional literal part is analysed exactly like a literal call-site props map (itsui/event/ui/handler/ui/render-fncompile to committed callbacks, prop checks run,:key/:refextract); the runtime map is an opaque foreign-boundary map that passes through unconverted (verbatim author-key names — a foreign head owns its own prop ABI) and marks the site:dynamic. The compiled literal props win any collision (the forwarded map is layered under them, mirroringspread-safe's owned-wins, minus the deny law). An internal view rejectsui/spread—:rf.ui.compile/spread-internal-view, it requires a literal props map (its per-slot memo comparator and slot ABI need the literal keys). - Errors: direct call →
:rf.error/ui-spread-outside-template;ui/spreadat an internal-view call site →:rf.ui.compile/spread-internal-view.
spread-safe¶
- Kind: function (template interop form)
- Signature:
(spread-safe owned caller) - Description: The literal safe-spread policy — the one attr passthrough a
component library can forward onto an internal element without clobbering owned props
or forfeiting the controlled guarantee.
ownedis a literal map of the component's own props (analysed like an element's props map, so a controlled owned site — a literal:value/:checkedco-present with a handler — retains the sync door);calleris the runtime attr map forwarded from the consumer. The compiler-visible deny law, enforced in every build (not dev-only): the structural / controlled / identity keys:key:ref:value:checkedand the component's owned:on-*handlers may not appear incaller— a literal offender is the compile error:rf.ui.compile/spread-safe-owned-key, a runtime offender throws:rf.error/ui-tree-malformed. Everything else passes and converts per the 004B rule table; owned props win any collision and:classcomposes (owned classes first). Generalspreadremains the visible-cost escape and still forfeits the sync door. Legal only in a DOM / custom-element props position. - Errors: direct call →
:rf.error/ui-spread-outside-template.
event¶
- Kind: function (compile-time authoring form)
- Signature:
(event [e] body…) - Description: Committed
:on-*handler form for a DOM / custom-element site whose body needs the live native event. The body runs when the callback fires; its result is the event vector to dispatch, ornilto dispatch nothing (a filter). The callback sees the view's committed values and dispatches to the committed frame — the same per-site-stable, retarget-safe machinery a literal event vector rides. At a compiler-proven controlled site (a literal:value/:checkedco-present with the handler on:on-input/:on-change/:on-before-input) a synchronouseventwhose result is a vector rides the one synchronous door alongside a literal-vector handler. Any synchronous result other than a vector ornilis a loud runtime diagnostic. - Errors: direct call →
:rf.error/ui-tree-malformed. - Example:
handler¶
- Kind: function (compile-time authoring form)
- Signature:
(handler [x] body…) - Description: The explicit imperative committed callback — the imperative
sibling of
event. The body runs after commit and its return is ignored (no dispatch of a returned vector — that isevent); it does imperative work. Like every committed callback it is per-site stable and reads the committed values (the stale-closure boundary law). Legal at a DOM / custom-element:on-*site (the explicit spelling of the bare-fn shorthand; the native event binds its parameter) and at a foreign-component or internal-view prop (the invoker's arguments bind through). At an internal-view seam it gives a bare fn prop the per-site stable identity a fresh closure lacks (C-13a). - Errors: direct call →
:rf.error/ui-tree-malformed. - Example:
render-fn¶
- Kind: function (compile-time authoring form)
- Signature:
(render-fn [args…] template) - Description: A pure render callback for an internal library seam. The body is
a template lexically visible at the consumer call site, so both emitters compile it
(closed grammar, no runtime hiccup); it renders from its arguments alone. A render-fn
value is legal in exactly two positions: a component call-site prop value
(
[list {:row (ui/render-fn [i x] …)}]) and aslotargument; the library invokes it throughslot. A render-fn body is pure render phase —sub/frameare allowed, butdispatch/ hooks / local state / effects inside are compile errors (a stateful replacement part is a pure slot body that mounts a staticdefview, which owns its state). - Errors: direct call →
:rf.error/ui-tree-malformed.
slot¶
- Kind: function (compile-time authoring form)
- Signature:
(slot render-fn-value arg…) - Description: The compiler-owned invocation of a
render-fnvalue at a library seam.render-fn-valueis arender-fn(inline or carried through a prop) ornil; any other value is a loud didactic error (:rf.error/ui-tree-malformed).nilrenders nothing; a render-fn renders with the supplied args, and its output participates in the surrounding children exactly like any other child. Legal only in a child position. - Errors: direct call →
:rf.error/ui-tree-malformed.
error-boundary¶
- Kind: function (compile-time authoring form)
- Signature:
(error-boundary {:fallback view :reset-key val :on-error [:ev …]} child) - Description: The explicit error component. Catches render / lifecycle throws
below it (React does not catch event-handler or async errors — those keep their
own typed paths). On catch the
:fallbackview renders with:error+ its declared props and cannot recursively dispatch.:on-error(optional) dispatches after the failing commit through a live frame captured at the owning view's render — never during render — with the caught error appended to the authored event vector. Changing:reset-key(comparedrf=) clears the caught error (retry = a state change that changes the key). The JVM / SSR renders the child under the server failure policy — boundaries are a client recovery mechanism. - Errors: bad opts / non-view fallback →
:rf.ui.compile/bad-error-boundary; direct call →:rf.error/ui-tree-malformed.
client-only¶
- Kind: function (compile-time authoring form)
- Signature:
(client-only {:fallback tpl} client-tpl) - Description: A browser-only subtree with a mandatory, capability-free
fallback (compiler-checked: the fallback carries no reactive read / host state /
effect / event handler, so the JVM / SSR and first hydration render it
deterministically). The JVM / SSR renders the fallback (a
:rf.ui/boundary :client-onlynode); the browser renders the client subtree (the S3 activation). The one root phase-flip that swaps all sites in a single update completes S5. - Errors: bad opts / missing fallback →
:rf.ui.compile/bad-client-only; a capability in the fallback →:rf.ui.compile/capability-in-fallback; direct call →:rf.error/ui-tree-malformed.
route-link¶
- Kind: compiled view (an ordinary
defview) - Signature:
[route-link {:to :route-id …html-attrs} & children] - Description: A navigation anchor — the compiled counterpart of the stock-Reagent
rf/route-link. It renders a real<a href=…>with the route's strategy-encoded href (so copy-link / open-in-new-tab / keyboard activation / no-JS navigation all work) and, on a plain in-app left click, dispatches:rf.route/url-requestedto the committed frame (tagged:source :router) instead of letting the browser reload.:tois required (a registered route id);:params/:query/:fragmentfeed both the href and the dispatch payload; every other key —:class,:title,:id,:aria-label,:target,:download,:on-click, and any further HTML attribute — passes through to the<a>. A caller:on-clickruns first and may veto (prevent default); modifier / middle-click and native anchors (:targetother than_self, or:download) defer to the browser so its native affordances stand. - Architecture:
route-linkis an ordinary compileddefview— it gets JSX emission, the generated prop comparator, JVM-tree parity, and dev identity for free; there is no route-link compiler intrinsic. The routing calculation (href encode, dispatch payload, native-attr detection) and the click law live in the optionalre-frame.routingartefact behind the substrate-neutral:routing/link-model/:routing/activate-link!late-bound seam, consumed through core's late-bind registry — sore-frame.uinever statically requires routing (ui -> core late-bind <- routing). Migrating a routed Reagent app is a mechanicalrf/route-link→ui/route-linkhead-rename. - JVM / SSR: renders the handler-free path-form
<a href>shell (no raw host event enters the server tree); the hydrated client re-encodes through the frame's URL strategy. The behavioural contract + full conformance matrix live in Spec 012 — routing owns the law. - Errors: rendered without
day8/re-frame2-routingon the classpath →:rf.error/routing-artefact-missing(naming the artefact, its Maven coord, and the link site). A plain[:a]stays available for intentional browser-native navigation.
->react¶
- Kind: function (outward interop bridge)
- Signature:
(->react view) → React component - Description: Export a compiled
view(adefviewvalue) as a React component a foreign React/UIx tree can render — the OUTWARD half of the foreign boundary (rawis the inward half). The incremental-adoption bridge:(def CartRow (ui/->react cart-row)), then renderCartRowanywhere in the legacy/foreign tree. - Memoised per view identity: repeated
(->react view)returns the identical component object, so a foreign parent re-render never remounts the exported subtree. - No root, no manifest, no preflight: the exported subtree renders inside the root the foreign parent owns; frame creation stays with the host app's boot/event code — an exported view scopes and resolves frames, it never creates them.
- Frame: the reserved
frameprop is resolved by own-property presence, not truthiness. Omitted (no ownframekey) is the sole ambient-resolution case — the exported view resolves its frame by the ordinary ambient chain (aframe-provider/frame-rootabove it), or fails loud with:rf.error/no-frame-context. An ownframeprop (a frame-id keyword or live frame value) scopes the subtree without owning it, and is always validated against the one frame-target grammar — including an explicitframe={null}/frame={undefined}, which fails loud rather than silently adopting the ambient frame. Every typed failure namesre-frame.ui/->react(notframe-provider), so the error lands at the bridge the caller used. - Props — one shallow rule: each prop maps to the view's prop-ABI slot by exact name
(write the slot names directly);
childrenandrefpass through preserved; only the reservedframeprop is consumed by the bridge. No camelisation, no deep conversion. - JVM: a call is a host-op error (
:rf.error/jvm-host-op) — a React component export has no meaning in a structural render. SSR through the bridge is unsupported in v1. - Errors: a non-view argument (a keyword,
nil, …) →:rf.error/ui-tree-malformed. An ownframeprop that is empty (null/undefined) →:rf.error/no-frame-context; malformed (not a frame-id keyword / live frame value) →:rf.error/bad-frame-provider-arg; a keyword / frame value naming no live frame →:rf.error/frame-provider-frame-absent. All attribute tore-frame.ui/->react.
presence¶
- Kind: function (compile-time authoring form)
- Signature:
(presence {:timeout-ms n} keyed-children) - Description: Declarative enter/exit retention, deliberately bounded — not an
animation system. Keyed children pass
:mounting→:present→:unmounting; an exiting child is retained for exactly the mandatory:timeout-ms— the exit retention duration and terminal bound — then removal is terminal and exactly-once (all ownership released). Removing then re-inserting a key interrupts the exit and re-enters. A child reads its phase withpresence-phase. On the JVM / SSR the structural render yields:present(there is no lifecycle to retain). The boundary is DOM-agnostic: no wrapper node, no stamped attributes, no observed DOM events — a presence-aware child owns its own exit styling and accessibility (stampinert/aria-hiddenand the exit class againstpresence-phase=:unmounting; the child's stylesheet ownsprefers-reduced-motion). - Errors: missing / non-positive
:timeout-ms→:rf.ui.compile/bad-presence; an unkeyed child under the boundary →:rf.ui.compile/presence-unkeyed-child; direct call →:rf.error/ui-tree-malformed.
presence-phase¶
- Kind: function (render-time read)
- Signature:
(presence-phase)→:mounting/:present/:unmounting - Description: The single presence-phase read. Inside a
(presence …)boundary it returns the child's live phase; outside one it returns:present(so presence-aware children stay reusable anywhere). A render-time read (a React context read on CLJS); the JVM structural render always yields:present.
Roots and mounting¶
mount¶
- Kind: macro
- Signature:
- Description: One-shot client mount: create-root + frame preflight +
render!. Idempotent per root (same root-id + container re-renders; live frames are not re-seeded).root-formmust be a literal vector. Identity opts (:root-id,:disambiguator,:identifier-prefix) are compile-time literals. Host error callbacks (:on-uncaught-error, …) are plain fns. Returns the Root. If a first render throws after allocation, the exact id/container/prefix claim is marked:tearing-downbefore cleanup. A normal cleanup releases at the next FIFO microtask; a cleanup throw leaves the claim quarantined, force-deads framework ownership for that incarnation, and rides the original mount error asrfUiRollbackCleanupErrorsecondary evidence (the mount error remains primary). - Example:
create-root¶
- Kind: macro
- Signature:
(create-root dom-node opts) → Root - Description: Fix root identity for the Root's lifetime without rendering.
Authored
:root-idis required (no root form to derive from). Prefermountfor the one-liner path.
render!¶
- Kind: macro
- Signature:
(render! root root-form) - Description: Render / re-render a literal root form into an existing Root. Frame plans in the top region preflight before render. Identity is unchanged.
hydrate-root¶
- Kind: macro
- Signature:
- Description: Hydrating mount. Identity comes from the server manifest, not
client identity opts (passing
:root-id/:identifier-prefixis an error).optsis host-behaviour tier only (:on-uncaught-error/:on-caught-error/:on-recoverable-error). A hydrate with no valid server manifest fails loud with:rf.error/root-manifest-invalid. - Canonical SSR boot — two calls. Seed state with
ssr/hydrate!without:render-tree-fn(a compiled root has no hashable client render-tree, so the:render-tree-fnhash channel is hiccup-tier-only), then adopt the server DOM:
(ssr/hydrate! {:frame :app :payload payload}) ;; state only — no :render-tree-fn
(ui/hydrate-root el
[ui/frame-provider {:frame :app} [app-root]]
{:on-recoverable-error (fn [error info] …)}) ;; optional host hook
Verification is by React-native adoption, not a hash: React diffs this root's
first :server-phase render (its ui/client-only fallbacks) against the server DOM,
and the runtime surfaces a React-recoverable adoption error (a text-content or
structural mismatch) as a :rf.ssr/hydration-mismatch diagnostic, composed over
any authored :on-recoverable-error (framework emit first, then your callback — never
clobbered). Attribute-only mismatches (a stale class / style / ARIA value) are
not surfaced — React takes its development-only warning path for those and
re-frame2 emits no trace on this tier (see
Spec 011 §Hydration-mismatch detection).
unmount!¶
- Kind: function
- Signature:
(unmount! root) → nil - Description: Total, exact-incarnation teardown. The root's complete
id/container/identifier-prefix claim moves
:live → :tearing-down → :released; release occurs at host settlement, not merely when.unmountreturns. Synchronous cleanup releases inline. A deferred React teardown retains the claim until its FIFO settlement microtask, rejecting same-id/container reuse in the interim. A throwing host cleanup force-deads that incarnation's framework owners but leaves the host claim quarantined:tearing-downbecause the container is unproven; a second call is a no-op and recovery uses a fresh container. Idempotent. Client-only; JVM host-op error.
render-static¶
- Kind: macro
- Signature:
(render-static root-form) → string - Description: The pure
:server-phase static-HTML render — the compiled-view counterpart of React'srenderToStaticMarkup. Renders a compiled root to an inert HTML string, non-hydrating: no manifest, no hydration payload, and no phase flip (aclient-onlysite renders its capability-free fallback and stops there). It is the static-page path, not the SSR-then-hydrate path (hydrate-root+re-frame.ssr/hydrate!own that). Like the other root macros it enforces the literal root form at the call site (a runtime-assembled vector is:rf.ui.compile/runtime-root-form), and the form mounts exactly one view (its derived identity). JVM/server only — a CLJS expansion is a compile error. The JVM structural tree it produces is folded to HTML byre-frame.ssr/emit-ui-tree, sore-frame.uinever statically requires the SSR artefact.
frame-root¶
- Kind: function (root-form template symbol)
- Signature:
(frame-root {:id … :initial-events …} & children) - Description: Static ENSURE plan wrapper, legal only in the top region of
a root form passed to
mount/render!/hydrate-root.:idmust be a compile-time keyword. Creates the frame if absent, seeds:initial-eventsonce, never destroys on unmount. Compiles away — direct call fails with:rf.error/ui-frame-root-outside-root-form. To scope an already-live frame inside a view tree, useframe-provider.
frame-provider¶
- Kind: function (template symbol)
- Signature:
(frame-provider {:frame f} & children) - Description: Scope a subtree to an already-live frame through React
context. Creates nothing, seeds nothing, destroys nothing.
:frameis a runtime frame id or live frame value. Absent frame →:rf.error/frame-provider-frame-absent. Direct call fails with:rf.error/ui-frame-provider-outside-template.
See also¶
re-frame.core— events, subs, frames (make-frame,capture-frame), boot (init!)- Introduction — the event pipeline this substrate plugs into
- Substrate design / UI guide under the synthesis suite (force-tracked findings) when
reader-facing
docs/ui/is not yet landed