Install and boot¶
You want Freehand on the page. Four moves: depend → install an adapter → mount → tear down. Everything upstream of the view is ordinary re-frame2 and does not change.
Pre-alpha: no Clojars coordinate
day8/re-frame2-freehand is not published, and there is no date at which
it will be. It lives in the re-frame2 monorepo, so today you resolve it — and
day8/re-frame2 with it — from a checkout using :local/root. Treat the
snippets below as the shape of the dependency, not as a coordinate you can
paste into a fresh project.
Depend on the artefact¶
;; deps.edn — resolved from a re-frame2 checkout beside your project
{:deps {day8/re-frame2 {:local/root "../re-frame2/implementation/core"}
day8/re-frame2-freehand {:local/root "../re-frame2/implementation/freehand"}}}
Two artefacts, and that is the whole floor: Freehand ships its own reactive-substrate
adapter, so a Freehand app needs no wrapper-library artefact. Two optional artefacts
join the same way when you need them: day8/re-frame2-routing for
v/route-link, and day8/re-frame2-ssr for
server rendering.
Boot: adapter, then mount¶
(ns my.app
(:require [re-frame.core :as rf]
[re-frame.freehand :as v]))
(v/defview app-root [_]
[:main [counter {:label "hello"}]])
(defn ^:export run []
(rf/init! v/adapter)
(v/mount [app-root {}]
(js/document.getElementById "app")
{:frame {:id :my.app/main :initial-events [[:app/init]]}}))
Alias Freehand as v. That alias is load-bearing beyond taste: ::v/value
only reads as :re-frame.freehand/value when v is the alias in that namespace.
Why an adapter at all, when Freehand renders itself¶
Because an adapter is not a renderer. Freehand puts elements on the page through
react-dom/client; what re-frame2's reactive substrate contract asks for is the
observation half — the container app-db lives in, and a derived value that says when
it moved. The two are independent, and until (rf/init! …) has run, minting a frame
raises :rf.error/no-adapter-installed.
v/adapter is Freehand's own, and it is the one to install on a new app. It is built
on the same React spine every adapter in the repo shares and adds no dependency of its
own — Freehand already requires react and react-dom/client. Two things it does that
matter to you: (rf/destroy-adapter!) unmounts every live Freehand root before it
disposes anything, and its flush-render! returns with the page settled rather than
leaving the repaint to a microtask.
That settled return is a promise about a drain the flush itself opens. Forced from
inside one that is already open — an event handler, or anything it calls, reaching
for flush-render! — it is refused with :rf.error/flush-in-open-epoch instead,
because rendering there would publish half-settled state: queued events whose update
and commit phases have not run yet. There is nothing to recover; let the drain reach
quiescence and flush once.
Unmount your roots with v/unmount! before destroying an adapter. The drain above
is a safety net — it stops a root you forgot from holding subscriptions and DOM past
the adapter's life — but only v/unmount! runs while the adapter is live, so only
v/unmount! destroys a frame a root ensured.
Already on Reagent or UIx? Install that adapter instead — (rf/init!
uix/adapter) — and add its artefact to deps.edn. One adapter per process, so a
mixed page picks one, and it can perfectly well be the wrapper's. Mount and unmount
your Freehand roots with v/mount / v/unmount! either way, and unmount them before
you destroy the adapter. The one thing that does not carry across: a wrapper adapter's
own synchronous test helpers do not settle Freehand's view cells.
Install it once, at boot, before the first frame exists. It is idempotent, so hot reload is safe.
Why the frame is seeded at the mount¶
v/mount's third argument is a closed options map, and :frame is the one you
will use on day one. Given a make-frame options map carrying :id, the root
ensures that frame and owns its lifetime; given a bare frame-id keyword, the
root scopes a frame something else owns.
The plan runs to completion before React is handed anything, so a view whose
body reads a subscription on its first render finds app-db already seeded. That
is what stops the empty-then-flash first paint — you do not need a dispatch-sync
before mount.
Teardown¶
v/mount returns a root handle. Unmounting releases the root's id, container and
identifierPrefix claims, disconnects every view boundary below it, and destroys
the frame if this root ensured it — a frame it merely scoped is left alone.
It is guarded rather than throwing: unmounting twice, or unmounting a root a newer
root has superseded, is a no-op.
Day-one checklist¶
[re-frame.freehand :as v]in the namespace- One
(rf/init! v/adapter)at boot, before any frame is minted (v/mount [root {}] el {:frame {:id … :initial-events […]}})(v/unmount! root)on teardown
Two roots on one page¶
A root's identity is derived from the mounted view's registered id, so a single root authors nothing. Mount the same view twice and the derivation collides — say so at the second site:
(v/mount [panel {:side :left}] left {:disambiguator :left})
(v/mount [panel {:side :right}] right {:root-id :shop/right})
A root claims its id, its container and its identifierPrefix before it renders
anything, so a collision fails loud (:rf.error/duplicate-root-id,
:rf.error/root-container-in-use) with the roots already on the page untouched.
Re-mounting the same root-id into the same container re-renders the live root rather than allocating a second one. That is the hot-reload path: a reload mints a fresh descriptor for the redefined view, but the qualified id it keys on does not move, so app-db survives the save.
Shadow build settings¶
Interpreted views need no build configuration at all. The compiled tier does: its analyzer and registries run at build time, behind one Shadow hook.
Configure it once in :build-defaults rather than per build. There is no
:cache-blockers tax — the hook harvests from cache-durable carriers, so a warm
daemon start reuses Shadow's disk cache.
Framework views¶
Two of the descriptors on the door reach into sibling artefacts, and each fails loudly rather than degrading if that artefact is absent:
| Verb | Needs | Absent |
|---|---|---|
v/route-link |
day8/re-frame2-routing |
:rf.error/routing-artefact-missing — never a dead link |
v/render-static |
day8/re-frame2-ssr at render time |
:rf.error/ssr-artefact-missing, naming the coordinate |
re-frame.freehand takes no compile-time require on either, so a namespace that
requires only Freehand still compiles.
If something feels wrong¶
| Symptom | Cause | Fix |
|---|---|---|
:rf.error/no-adapter-installed |
no rf/init! before the first frame |
install an adapter at boot |
| First paint empty, then values appear | seeding after mount | seed with the mount's :frame :initial-events |
:rf.error/duplicate-root-id |
one view mounted twice | :disambiguator or an explicit :root-id |
::v/value is not filling |
the namespace does not alias Freehand as v |
[re-frame.freehand :as v] |
| Hot reload wipes app-db | remount into a fresh frame | same root-id, same container — the reload re-renders in place |